centaurus-cli 3.1.3 → 3.1.5

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 (138) hide show
  1. package/dist/cli-adapter.js +685 -153
  2. package/dist/cli-adapter.js.map +1 -1
  3. package/dist/config/defaultConfig.js +1 -4
  4. package/dist/config/defaultConfig.js.map +1 -1
  5. package/dist/config/models.js +4 -0
  6. package/dist/config/models.js.map +1 -1
  7. package/dist/config/slash-commands.js +66 -2
  8. package/dist/config/slash-commands.js.map +1 -1
  9. package/dist/config/types.js +4 -4
  10. package/dist/config/types.js.map +1 -1
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/services/ai-context-injector.js +109 -0
  14. package/dist/services/ai-context-injector.js.map +1 -1
  15. package/dist/services/api-client.js.map +1 -1
  16. package/dist/services/background-task-manager.js +59 -0
  17. package/dist/services/background-task-manager.js.map +1 -1
  18. package/dist/services/local-chat-storage.js +2 -0
  19. package/dist/services/local-chat-storage.js.map +1 -1
  20. package/dist/services/skill-storage.js +141 -0
  21. package/dist/services/skill-storage.js.map +1 -0
  22. package/dist/services/sub-agent-manager.js +49 -8
  23. package/dist/services/sub-agent-manager.js.map +1 -1
  24. package/dist/services/warpify-detector.js +17 -5
  25. package/dist/services/warpify-detector.js.map +1 -1
  26. package/dist/tools/background-command.js +5 -2
  27. package/dist/tools/background-command.js.map +1 -1
  28. package/dist/tools/command.js +367 -109
  29. package/dist/tools/command.js.map +1 -1
  30. package/dist/tools/file-ops.js +23 -6
  31. package/dist/tools/file-ops.js.map +1 -1
  32. package/dist/tools/plan-mode.js +184 -336
  33. package/dist/tools/plan-mode.js.map +1 -1
  34. package/dist/tools/sub-agent.js +24 -5
  35. package/dist/tools/sub-agent.js.map +1 -1
  36. package/dist/tools/todo-list.js +157 -0
  37. package/dist/tools/todo-list.js.map +1 -0
  38. package/dist/types/skill.js +30 -0
  39. package/dist/types/skill.js.map +1 -0
  40. package/dist/ui/components/App.js +956 -162
  41. package/dist/ui/components/App.js.map +1 -1
  42. package/dist/ui/components/AuthScreen.js +3 -1
  43. package/dist/ui/components/AuthScreen.js.map +1 -1
  44. package/dist/ui/components/AuthWelcomeScreen.js +3 -1
  45. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  46. package/dist/ui/components/CodeBlock.js +3 -1
  47. package/dist/ui/components/CodeBlock.js.map +1 -1
  48. package/dist/ui/components/CompactShellPreview.js +44 -0
  49. package/dist/ui/components/CompactShellPreview.js.map +1 -0
  50. package/dist/ui/components/ConfigViewer.js +3 -1
  51. package/dist/ui/components/ConfigViewer.js.map +1 -1
  52. package/dist/ui/components/ConfirmPrompt.js +3 -1
  53. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  54. package/dist/ui/components/ConnectionStatusMessage.js +3 -1
  55. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  56. package/dist/ui/components/DetailedPlanReviewScreen.js +84 -74
  57. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  58. package/dist/ui/components/DiffViewer.js +6 -3
  59. package/dist/ui/components/DiffViewer.js.map +1 -1
  60. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  61. package/dist/ui/components/FileTagAutocomplete.js +4 -2
  62. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  63. package/dist/ui/components/InputBox.js +243 -40
  64. package/dist/ui/components/InputBox.js.map +1 -1
  65. package/dist/ui/components/InteractiveShell.js +5 -3
  66. package/dist/ui/components/InteractiveShell.js.map +1 -1
  67. package/dist/ui/components/KeyboardHelp.js +4 -1
  68. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  69. package/dist/ui/components/LoadingIndicator.js +3 -1
  70. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  71. package/dist/ui/components/MCPAddScreen.js +63 -13
  72. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  73. package/dist/ui/components/MarkdownRenderer.js +3 -1
  74. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  75. package/dist/ui/components/MessageDisplay.js +9 -7
  76. package/dist/ui/components/MessageDisplay.js.map +1 -1
  77. package/dist/ui/components/ModelPicker.js +170 -0
  78. package/dist/ui/components/ModelPicker.js.map +1 -0
  79. package/dist/ui/components/MonitorModeAIPanel.js +3 -1
  80. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  81. package/dist/ui/components/PlanAcceptedMessage.js +12 -6
  82. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  83. package/dist/ui/components/PlanQuestionMessage.js +37 -0
  84. package/dist/ui/components/PlanQuestionMessage.js.map +1 -0
  85. package/dist/ui/components/PlanQuestionScreen.js +138 -0
  86. package/dist/ui/components/PlanQuestionScreen.js.map +1 -0
  87. package/dist/ui/components/PlanReviewScreen.js +7 -9
  88. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  89. package/dist/ui/components/RulesEditorScreen.js +65 -28
  90. package/dist/ui/components/RulesEditorScreen.js.map +1 -1
  91. package/dist/ui/components/SelectPrompt.js +3 -1
  92. package/dist/ui/components/SelectPrompt.js.map +1 -1
  93. package/dist/ui/components/SkillCreatorScreen.js +217 -0
  94. package/dist/ui/components/SkillCreatorScreen.js.map +1 -0
  95. package/dist/ui/components/SlashCommandAutocomplete.js +4 -2
  96. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  97. package/dist/ui/components/StatusBar.js +4 -2
  98. package/dist/ui/components/StatusBar.js.map +1 -1
  99. package/dist/ui/components/StreamingMessageDisplay.js +5 -3
  100. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  101. package/dist/ui/components/SubAgentListScreen.js +65 -0
  102. package/dist/ui/components/SubAgentListScreen.js.map +1 -0
  103. package/dist/ui/components/SubAgentViewScreen.js +123 -0
  104. package/dist/ui/components/SubAgentViewScreen.js.map +1 -0
  105. package/dist/ui/components/TaskCompletedMessage.js +40 -8
  106. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  107. package/dist/ui/components/TaskProgressIndicator.js +6 -4
  108. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  109. package/dist/ui/components/TextEditor.js +297 -0
  110. package/dist/ui/components/TextEditor.js.map +1 -0
  111. package/dist/ui/components/TodoListMessage.js +59 -0
  112. package/dist/ui/components/TodoListMessage.js.map +1 -0
  113. package/dist/ui/components/ToolExecutionMessage.js +134 -84
  114. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  115. package/dist/ui/components/ToolExecutionStatus.js +3 -1
  116. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  117. package/dist/ui/components/WelcomeBanner.js +33 -33
  118. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  119. package/dist/ui/components/WorkflowCreatorScreen.js +5 -3
  120. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  121. package/dist/ui/theme.js +97 -0
  122. package/dist/ui/theme.js.map +1 -0
  123. package/dist/ui/utils/chat-history-limit.js +247 -0
  124. package/dist/ui/utils/chat-history-limit.js.map +1 -0
  125. package/dist/utils/chat-formatter.js +22 -9
  126. package/dist/utils/chat-formatter.js.map +1 -1
  127. package/dist/utils/input-classifier.js +11 -1
  128. package/dist/utils/input-classifier.js.map +1 -1
  129. package/dist/utils/output-truncation.js +175 -0
  130. package/dist/utils/output-truncation.js.map +1 -0
  131. package/dist/utils/rule-reference-resolver.js +3 -3
  132. package/dist/utils/rule-reference-resolver.js.map +1 -1
  133. package/dist/utils/tunnel-commands-manager.js +134 -0
  134. package/dist/utils/tunnel-commands-manager.js.map +1 -0
  135. package/package.json +91 -90
  136. package/postinstall.js +4 -11
  137. package/dist/ui/components/MultiLineInput.js +0 -255
  138. package/dist/ui/components/MultiLineInput.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/MessageDisplay.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport { Message } from '../../types/index.js';\r\nimport { ToolExecutionMessage } from './ToolExecutionMessage.js';\r\nimport { MarkdownRenderer } from './MarkdownRenderer.js';\r\nimport { ConnectionStatusMessage } from './ConnectionStatusMessage.js';\r\n\r\ninterface MessageDisplayProps {\r\n message: Message;\r\n /** Total files in messages before this one (for global numbering) */\r\n fileCountBefore?: number;\r\n}\r\n\r\n/**\r\n * Parse [IMAGE: ...] and [FILE: ...] markers from content\r\n * Also handles uploading state (gs://uploading) and failed uploads (upload failed)\r\n * Returns { textContent: string without markers, files: array of { type, format } }\r\n */\r\nfunction parseFileMarkers(content: string): { textContent: string; files: Array<{ type: string; format: string }> } {\r\n // Pattern to match [IMAGE: filename (gs://...)] or [FILE: filename (upload failed...)] or [FILE: ... (gs://uploading)]\r\n const fileMarkerRegex = /\\[(IMAGE|FILE):\\s*([^\\]]+?)\\s*\\((gs:\\/\\/[^\\)]+|upload failed[^\\)]*|uploading[^\\)]*)\\)\\]/g;\r\n const files: Array<{ type: string; format: string }> = [];\r\n\r\n let match;\r\n while ((match = fileMarkerRegex.exec(content)) !== null) {\r\n const type = match[1]; // IMAGE or FILE\r\n const filename = match[2].trim();\r\n // Extract extension from filename\r\n const ext = filename.split('.').pop()?.toLowerCase() || 'file';\r\n files.push({ type, format: ext });\r\n }\r\n\r\n // Remove markers from content\r\n let textContent = content\r\n .replace(/\\[(IMAGE|FILE):\\s*([^\\]]+?)\\s*\\((gs:\\/\\/[^\\)]+|upload failed[^\\)]*|uploading[^\\)]*)\\)\\]/g, '')\r\n .trim();\r\n\r\n return {\r\n textContent,\r\n files\r\n };\r\n}\r\n\r\n/**\r\n * Count files in a message content\r\n */\r\nexport function countFilesInMessage(content: string): number {\r\n const fileMarkerMatches = content.match(/\\[(IMAGE|FILE):\\s*[^\\]]+?\\s*\\((gs:\\/\\/[^\\)]+|upload failed[^\\)]*|uploading[^\\)]*)\\)\\]/g) || [];\r\n return fileMarkerMatches.length;\r\n}\r\n\r\n/**\r\n * File attachment badge component\r\n * Shows a styled box with file number and format\r\n */\r\nconst FileAttachmentBadge: React.FC<{ fileNumber: number; type: string; format: string }> = ({ fileNumber, type, format }) => {\r\n const label = type === 'IMAGE' ? 'Image' : 'File';\r\n return (\r\n <Box\r\n borderStyle=\"round\"\r\n borderColor=\"#555555\"\r\n paddingX={1}\r\n marginRight={1}\r\n marginTop={1}\r\n >\r\n <Text color=\"#aaaaaa\">{label} {fileNumber} </Text>\r\n <Text color=\"#666666\">[{format}]</Text>\r\n </Box>\r\n );\r\n};\r\n\r\n/**\r\n * Render text content, highlighting @file-tag tokens in cyan so they stand\r\n * out the same way they do in the input bar.\r\n */\r\nfunction renderTextWithFileTags(text: string): React.ReactNode[] {\r\n const tagRegex = /(@[^\\s@]+)/g;\r\n const parts: React.ReactNode[] = [];\r\n let lastIndex = 0;\r\n let match: RegExpExecArray | null;\r\n let key = 0;\r\n\r\n while ((match = tagRegex.exec(text)) !== null) {\r\n if (match.index > lastIndex) {\r\n parts.push(\r\n <Text key={key++} color=\"#ffffff\">{text.slice(lastIndex, match.index)}</Text>\r\n );\r\n }\r\n parts.push(\r\n <Text key={key++} color=\"#00ccff\" bold>{match[1]}</Text>\r\n );\r\n lastIndex = match.index + match[1].length;\r\n }\r\n\r\n if (lastIndex < text.length) {\r\n parts.push(\r\n <Text key={key++} color=\"#ffffff\">{text.slice(lastIndex)}</Text>\r\n );\r\n }\r\n\r\n return parts;\r\n}\r\n\r\nexport const MessageDisplay: React.FC<MessageDisplayProps> = React.memo(({\r\n message,\r\n fileCountBefore = 0\r\n}) => {\r\n const [selectedOption, setSelectedOption] = React.useState<number>(0);\r\n const [isApproved, setIsApproved] = React.useState<boolean | null>(null);\r\n\r\n const getRoleColor = (role: string) => {\r\n switch (role) {\r\n case 'user': return '#00ccff';\r\n case 'assistant': return '#00cc66';\r\n case 'system': return '#ffaa00';\r\n case 'tool': return '#00ccff';\r\n default: return '#ffffff';\r\n }\r\n };\r\n\r\n const getRoleLabel = (role: string) => {\r\n switch (role) {\r\n case 'user': return 'You';\r\n case 'assistant': return 'Centaurus';\r\n case 'system': return 'System';\r\n case 'tool': return 'Tool';\r\n default: return role;\r\n }\r\n };\r\n\r\n const timestamp = message.timestamp\r\n ? new Date(message.timestamp).toLocaleTimeString()\r\n : '';\r\n\r\n // Handle approval input\r\n useInput((input, key) => {\r\n if (!message.approval || isApproved !== null) return;\r\n\r\n if (key.upArrow) {\r\n setSelectedOption(0);\r\n } else if (key.downArrow) {\r\n setSelectedOption(1);\r\n } else if (key.return) {\r\n const approved = selectedOption === 0;\r\n setIsApproved(approved);\r\n message.approval.resolve(approved);\r\n }\r\n }, { isActive: message.approval !== undefined && isApproved === null });\r\n\r\n // Render tool execution message using specialized component\r\n if (message.toolExecution) {\r\n return <ToolExecutionMessage message={message} />;\r\n }\r\n\r\n // Render connection status message using specialized component\r\n if (message.connectionStatus) {\r\n return <ConnectionStatusMessage status={message.connectionStatus} />;\r\n }\r\n\r\n // Approval requests should never be in message history\r\n // They are handled by ApprovalSection component that replaces the input bar\r\n if (message.approval) {\r\n return null;\r\n }\r\n\r\n // Skip rendering empty assistant messages that have no meaningful content\r\n // This prevents blank space from appearing above tool execution messages\r\n // (especially in WSL mode where tools take longer and show 'executing' state)\r\n if (message.role === 'assistant' &&\r\n !message.content.trim() &&\r\n message.thinkingDuration === undefined) {\r\n return null;\r\n }\r\n\r\n // Determine border color based on message type\r\n const getBorderColor = () => {\r\n if (message.isCommandMode) {\r\n return message.role === 'assistant' ? '#00cc66' : '#00ccff';\r\n }\r\n return message.role === 'assistant' ? '#00ccff' : '#666666';\r\n };\r\n\r\n const borderColor = getBorderColor();\r\n\r\n // Parse markers for user messages (only in AI mode, not command/shell mode)\r\n const { textContent, files } = message.role === 'user' && !message.isCommandMode\r\n ? parseFileMarkers(message.content)\r\n : { textContent: message.content, files: [] };\r\n\r\n // Generate file badges with global numbering (fileCountBefore + i + 1)\r\n const fileBadges = files.map((file, i) => (\r\n <FileAttachmentBadge\r\n key={`file-${message.id}-${i}`}\r\n fileNumber={fileCountBefore + i + 1}\r\n type={file.type}\r\n format={file.format}\r\n />\r\n ));\r\n\r\n // Regular message rendering\r\n return (\r\n <Box flexDirection=\"column\" marginBottom={1}>\r\n {!message.isSpacer && (\r\n <Box marginBottom={1}>\r\n <Text color={getRoleColor(message.role)} bold>\r\n {getRoleLabel(message.role)}:\r\n </Text>\r\n {timestamp && (\r\n <Text color=\"#666666\" dimColor> {timestamp}</Text>\r\n )}\r\n </Box>\r\n )}\r\n\r\n {/* Display thinking duration at the TOP for completed messages */}\r\n {message.thinkingDuration !== undefined && message.thinkingDuration >= 0 && (\r\n <Box marginBottom={1}>\r\n <Text dimColor>Thought for {message.thinkingDuration} second{message.thinkingDuration !== 1 ? 's' : ''} ›</Text>\r\n </Box>\r\n )}\r\n\r\n {/* Render content with left border for AI responses - only if there's content */}\r\n {message.role === 'assistant' ? (\r\n message.content.trim() ? (\r\n <Box\r\n borderStyle=\"single\"\r\n borderColor={borderColor}\r\n borderLeft={true}\r\n borderRight={false}\r\n borderTop={false}\r\n borderBottom={false}\r\n paddingLeft={1}\r\n flexDirection=\"column\"\r\n width=\"100%\"\r\n >\r\n <MarkdownRenderer content={message.content} maxWidth={(process.stdout.columns || 120) - 6} />\r\n </Box>\r\n ) : null\r\n ) : (\r\n <>\r\n {/* User message text (without file markers) */}\r\n {textContent.trim() ? (\r\n <Box paddingLeft={2}>\r\n <Text>{renderTextWithFileTags(textContent)}</Text>\r\n </Box>\r\n ) : null}\r\n\r\n {/* File attachment badges */}\r\n {fileBadges.length > 0 && (\r\n <Box paddingLeft={2} flexDirection=\"row\" flexWrap=\"wrap\">\r\n {fileBadges}\r\n </Box>\r\n )}\r\n </>\r\n )}\r\n </Box>\r\n );\r\n});\r\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/MessageDisplay.tsx"],"sourcesContent":["import React from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport { Message } from '../../types/index.js';\r\nimport { ToolExecutionMessage } from './ToolExecutionMessage.js';\r\nimport { MarkdownRenderer } from './MarkdownRenderer.js';\r\nimport { ConnectionStatusMessage } from './ConnectionStatusMessage.js';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface MessageDisplayProps {\r\n message: Message;\r\n /** Total files in messages before this one (for global numbering) */\r\n fileCountBefore?: number;\r\n}\r\n\r\n/**\r\n * Parse [IMAGE: ...] and [FILE: ...] markers from content\r\n * Also handles uploading state (gs://uploading) and failed uploads (upload failed)\r\n * Returns { textContent: string without markers, files: array of { type, format } }\r\n */\r\nfunction parseFileMarkers(content: string): { textContent: string; files: Array<{ type: string; format: string }> } {\r\n // Pattern to match [IMAGE: filename (gs://...)] or [FILE: filename (upload failed...)] or [FILE: ... (gs://uploading)]\r\n const fileMarkerRegex = /\\[(IMAGE|FILE):\\s*([^\\]]+?)\\s*\\((gs:\\/\\/[^\\)]+|upload failed[^\\)]*|uploading[^\\)]*)\\)\\]/g;\r\n const files: Array<{ type: string; format: string }> = [];\r\n\r\n let match;\r\n while ((match = fileMarkerRegex.exec(content)) !== null) {\r\n const type = match[1]; // IMAGE or FILE\r\n const filename = match[2].trim();\r\n // Extract extension from filename\r\n const ext = filename.split('.').pop()?.toLowerCase() || 'file';\r\n files.push({ type, format: ext });\r\n }\r\n\r\n // Remove markers from content\r\n let textContent = content\r\n .replace(/\\[(IMAGE|FILE):\\s*([^\\]]+?)\\s*\\((gs:\\/\\/[^\\)]+|upload failed[^\\)]*|uploading[^\\)]*)\\)\\]/g, '')\r\n .trim();\r\n\r\n return {\r\n textContent,\r\n files\r\n };\r\n}\r\n\r\n/**\r\n * Count files in a message content\r\n */\r\nexport function countFilesInMessage(content: string): number {\r\n const fileMarkerMatches = content.match(/\\[(IMAGE|FILE):\\s*[^\\]]+?\\s*\\((gs:\\/\\/[^\\)]+|upload failed[^\\)]*|uploading[^\\)]*)\\)\\]/g) || [];\r\n return fileMarkerMatches.length;\r\n}\r\n\r\n/**\r\n * File attachment badge component\r\n * Shows a styled box with file number and format\r\n */\r\nconst FileAttachmentBadge: React.FC<{ fileNumber: number; type: string; format: string }> = ({ fileNumber, type, format }) => {\r\n const label = type === 'IMAGE' ? 'Image' : 'File';\r\n return (\r\n <Box\r\n borderStyle=\"round\"\r\n borderColor=\"#555555\"\r\n paddingX={1}\r\n marginRight={1}\r\n marginTop={1}\r\n >\r\n <Text color=\"#aaaaaa\">{label} {fileNumber} </Text>\r\n <Text color=\"#666666\">[{format}]</Text>\r\n </Box>\r\n );\r\n};\r\n\r\n/**\r\n * Render text content, highlighting @file-tag tokens in cyan so they stand\r\n * out the same way they do in the input bar.\r\n */\r\nfunction renderTextWithFileTags(text: string, accentColor: string): React.ReactNode[] {\r\n const tagRegex = /(@[^\\s@]+)/g;\r\n const parts: React.ReactNode[] = [];\r\n let lastIndex = 0;\r\n let match: RegExpExecArray | null;\r\n let key = 0;\r\n\r\n while ((match = tagRegex.exec(text)) !== null) {\r\n if (match.index > lastIndex) {\r\n parts.push(\r\n <Text key={key++} color=\"#ffffff\">{text.slice(lastIndex, match.index)}</Text>\r\n );\r\n }\r\n parts.push(\r\n <Text key={key++} color={accentColor} bold>{match[1]}</Text>\r\n );\r\n lastIndex = match.index + match[1].length;\r\n }\r\n\r\n if (lastIndex < text.length) {\r\n parts.push(\r\n <Text key={key++} color=\"#ffffff\">{text.slice(lastIndex)}</Text>\r\n );\r\n }\r\n\r\n return parts;\r\n}\r\n\r\nexport const MessageDisplay: React.FC<MessageDisplayProps> = React.memo(({\r\n message,\r\n fileCountBefore = 0\r\n}) => {\r\n const theme = useTheme();\r\n const [selectedOption, setSelectedOption] = React.useState<number>(0);\r\n const [isApproved, setIsApproved] = React.useState<boolean | null>(null);\r\n\r\n const getRoleColor = (role: string) => {\r\n switch (role) {\r\n case 'user': return theme.accent;\r\n case 'assistant': return '#00cc66';\r\n case 'system': return '#ffaa00';\r\n case 'tool': return theme.accent;\r\n default: return '#ffffff';\r\n }\r\n };\r\n\r\n const getRoleLabel = (role: string) => {\r\n switch (role) {\r\n case 'user': return 'You';\r\n case 'assistant': return 'Centaurus';\r\n case 'system': return 'System';\r\n case 'tool': return 'Tool';\r\n default: return role;\r\n }\r\n };\r\n\r\n const timestamp = message.timestamp\r\n ? new Date(message.timestamp).toLocaleTimeString()\r\n : '';\r\n\r\n // Handle approval input\r\n useInput((input, key) => {\r\n if (!message.approval || isApproved !== null) return;\r\n\r\n if (key.upArrow) {\r\n setSelectedOption(0);\r\n } else if (key.downArrow) {\r\n setSelectedOption(1);\r\n } else if (key.return) {\r\n const approved = selectedOption === 0;\r\n setIsApproved(approved);\r\n message.approval.resolve(approved);\r\n }\r\n }, { isActive: message.approval !== undefined && isApproved === null });\r\n\r\n // Render tool execution message using specialized component\r\n if (message.toolExecution) {\r\n return <ToolExecutionMessage message={message} />;\r\n }\r\n\r\n // Render connection status message using specialized component\r\n if (message.connectionStatus) {\r\n return <ConnectionStatusMessage status={message.connectionStatus} />;\r\n }\r\n\r\n // Approval requests should never be in message history\r\n // They are handled by ApprovalSection component that replaces the input bar\r\n if (message.approval) {\r\n return null;\r\n }\r\n\r\n // Skip rendering empty assistant messages that have no meaningful content\r\n // This prevents blank space from appearing above tool execution messages\r\n // (especially in WSL mode where tools take longer and show 'executing' state)\r\n if (message.role === 'assistant' &&\r\n !message.content.trim() &&\r\n message.thinkingDuration === undefined) {\r\n return null;\r\n }\r\n\r\n // Determine border color based on message type\r\n const getBorderColor = () => {\r\n if (message.isCommandMode) {\r\n return message.role === 'assistant' ? '#00cc66' : theme.accent;\r\n }\r\n return message.role === 'assistant' ? theme.accent : '#666666';\r\n };\r\n\r\n const borderColor = getBorderColor();\r\n\r\n // Parse markers for user messages (only in AI mode, not command/shell mode)\r\n const { textContent, files } = message.role === 'user' && !message.isCommandMode\r\n ? parseFileMarkers(message.content)\r\n : { textContent: message.content, files: [] };\r\n\r\n // Generate file badges with global numbering (fileCountBefore + i + 1)\r\n const fileBadges = files.map((file, i) => (\r\n <FileAttachmentBadge\r\n key={`file-${message.id}-${i}`}\r\n fileNumber={fileCountBefore + i + 1}\r\n type={file.type}\r\n format={file.format}\r\n />\r\n ));\r\n\r\n // Regular message rendering\r\n return (\r\n <Box flexDirection=\"column\" marginBottom={1}>\r\n {!message.isSpacer && (\r\n <Box marginBottom={1}>\r\n <Text color={getRoleColor(message.role)} bold>\r\n {getRoleLabel(message.role)}:\r\n </Text>\r\n {timestamp && (\r\n <Text color=\"#666666\" dimColor> {timestamp}</Text>\r\n )}\r\n </Box>\r\n )}\r\n\r\n {/* Display thinking duration at the TOP for completed messages */}\r\n {message.thinkingDuration !== undefined && message.thinkingDuration >= 0 && (\r\n <Box marginBottom={1}>\r\n <Text dimColor>Thought for {message.thinkingDuration} second{message.thinkingDuration !== 1 ? 's' : ''} ›</Text>\r\n </Box>\r\n )}\r\n\r\n {/* Render content with left border for AI responses - only if there's content */}\r\n {message.role === 'assistant' ? (\r\n message.content.trim() ? (\r\n <Box\r\n borderStyle=\"single\"\r\n borderColor={borderColor}\r\n borderLeft={true}\r\n borderRight={false}\r\n borderTop={false}\r\n borderBottom={false}\r\n paddingLeft={1}\r\n flexDirection=\"column\"\r\n width=\"100%\"\r\n >\r\n <MarkdownRenderer content={message.content} maxWidth={(process.stdout.columns || 120) - 6} />\r\n </Box>\r\n ) : null\r\n ) : (\r\n <>\r\n {/* User message text (without file markers) */}\r\n {textContent.trim() ? (\r\n <Box paddingLeft={2}>\r\n <Text>{renderTextWithFileTags(textContent, theme.accent)}</Text>\r\n </Box>\r\n ) : null}\r\n\r\n {/* File attachment badges */}\r\n {fileBadges.length > 0 && (\r\n <Box paddingLeft={2} flexDirection=\"row\" flexWrap=\"wrap\">\r\n {fileBadges}\r\n </Box>\r\n )}\r\n </>\r\n )}\r\n </Box>\r\n );\r\n});\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,170 @@
1
+ import React, { useState, useMemo } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { useTheme } from "../theme.js";
4
+ const PROVIDER_META = {
5
+ google: { displayName: "Google", order: 0 },
6
+ anthropic: { displayName: "Anthropic", order: 1 },
7
+ bedrock: { displayName: "Amazon Bedrock", order: 2 },
8
+ maas: { displayName: "MaaS", order: 3 },
9
+ nvidia_nim: { displayName: "NVIDIA NIM", order: 4 },
10
+ silicon_flow: { displayName: "Silicon Flow", order: 5 },
11
+ openrouter: { displayName: "OpenRouter", order: 6 }
12
+ };
13
+ function getProviderDisplay(provider) {
14
+ return PROVIDER_META[provider]?.displayName || provider;
15
+ }
16
+ function getProviderOrder(provider) {
17
+ return PROVIDER_META[provider]?.order ?? 99;
18
+ }
19
+ function getDisplayProviderKey(m) {
20
+ return m.displayProvider || m.provider;
21
+ }
22
+ function buildEntries(models, currentModelName) {
23
+ const byProvider = /* @__PURE__ */ new Map();
24
+ for (const m of models) {
25
+ const key = getDisplayProviderKey(m);
26
+ const list = byProvider.get(key) || [];
27
+ list.push(m);
28
+ byProvider.set(key, list);
29
+ }
30
+ const sortedProviders = Array.from(byProvider.keys()).sort(
31
+ (a, b) => getProviderOrder(a) - getProviderOrder(b)
32
+ );
33
+ const entries = [];
34
+ for (const provider of sortedProviders) {
35
+ const providerModels = byProvider.get(provider);
36
+ entries.push({
37
+ type: "header",
38
+ label: getProviderDisplay(provider),
39
+ isCurrent: false
40
+ });
41
+ const grouped = /* @__PURE__ */ new Map();
42
+ const standalone = [];
43
+ for (const m of providerModels) {
44
+ if (m.group) {
45
+ const list = grouped.get(m.group) || [];
46
+ list.push(m);
47
+ grouped.set(m.group, list);
48
+ } else {
49
+ standalone.push(m);
50
+ }
51
+ }
52
+ for (const [groupKey, groupModels] of grouped) {
53
+ const baseName = groupModels[0].name.replace(/\s*\(.*?\)\s*$/, "");
54
+ const isAnyCurrent = groupModels.some((m) => m.name === currentModelName);
55
+ entries.push({
56
+ type: "group",
57
+ label: baseName,
58
+ description: groupModels[0].description.replace(/\s*\(.*?\)\s*$/, ""),
59
+ isCurrent: isAnyCurrent,
60
+ groupKey,
61
+ variants: groupModels.map((m) => ({
62
+ label: m.variantLabel || m.name,
63
+ description: m.description,
64
+ modelIndex: models.indexOf(m),
65
+ isCurrent: m.name === currentModelName
66
+ }))
67
+ });
68
+ }
69
+ for (const m of standalone) {
70
+ entries.push({
71
+ type: "model",
72
+ label: m.name,
73
+ description: m.description,
74
+ isCurrent: m.name === currentModelName,
75
+ modelIndex: models.indexOf(m)
76
+ });
77
+ }
78
+ }
79
+ return entries;
80
+ }
81
+ const ModelPicker = ({ models, currentModelName, onSelect, onCancel }) => {
82
+ const theme = useTheme();
83
+ const entries = useMemo(() => buildEntries(models, currentModelName), [models, currentModelName]);
84
+ const selectableIndices = useMemo(
85
+ () => entries.map((e, i) => e.type !== "header" ? i : -1).filter((i) => i >= 0),
86
+ [entries]
87
+ );
88
+ const [phase, setPhase] = useState("main");
89
+ const [focusPos, setFocusPos] = useState(0);
90
+ const [activeGroup, setActiveGroup] = useState(null);
91
+ const [variantFocusPos, setVariantFocusPos] = useState(0);
92
+ const maxVisible = 20;
93
+ useInput((input, key) => {
94
+ if (phase === "main") {
95
+ if (key.upArrow) {
96
+ setFocusPos((p) => Math.max(0, p - 1));
97
+ } else if (key.downArrow) {
98
+ setFocusPos((p) => Math.min(selectableIndices.length - 1, p + 1));
99
+ } else if (key.return) {
100
+ const entryIdx = selectableIndices[focusPos];
101
+ const entry = entries[entryIdx];
102
+ if (entry.type === "group" && entry.variants && entry.variants.length > 1) {
103
+ setActiveGroup(entry);
104
+ setVariantFocusPos(0);
105
+ setPhase("variant");
106
+ } else if (entry.type === "group" && entry.variants && entry.variants.length === 1) {
107
+ onSelect(`${entry.variants[0].modelIndex}`);
108
+ } else if (entry.type === "model" && entry.modelIndex !== void 0) {
109
+ onSelect(`${entry.modelIndex}`);
110
+ }
111
+ } else if (key.rightArrow) {
112
+ const entryIdx = selectableIndices[focusPos];
113
+ const entry = entries[entryIdx];
114
+ if (entry.type === "group" && entry.variants && entry.variants.length > 1) {
115
+ setActiveGroup(entry);
116
+ setVariantFocusPos(0);
117
+ setPhase("variant");
118
+ }
119
+ } else if (key.escape) {
120
+ onCancel();
121
+ }
122
+ } else if (phase === "variant" && activeGroup?.variants) {
123
+ if (key.upArrow) {
124
+ setVariantFocusPos((p) => Math.max(0, p - 1));
125
+ } else if (key.downArrow) {
126
+ setVariantFocusPos((p) => Math.min(activeGroup.variants.length - 1, p + 1));
127
+ } else if (key.return) {
128
+ const variant = activeGroup.variants[variantFocusPos];
129
+ onSelect(`${variant.modelIndex}`);
130
+ } else if (key.escape || key.leftArrow) {
131
+ setPhase("main");
132
+ setActiveGroup(null);
133
+ }
134
+ }
135
+ });
136
+ if (phase === "variant" && activeGroup?.variants) {
137
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: theme.accent, bold: true }, "Select Configuration for ", activeGroup.label), /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "\u2190 Press Esc or Left Arrow to go back"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, activeGroup.variants.map((v, i) => {
138
+ const isFocused = variantFocusPos === i;
139
+ return /* @__PURE__ */ React.createElement(Box, { key: i }, /* @__PURE__ */ React.createElement(Text, { color: isFocused ? theme.accent : "#666666" }, isFocused ? "\u276F " : " "), /* @__PURE__ */ React.createElement(Text, { color: isFocused ? theme.accent : void 0, bold: isFocused }, v.label), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " \u2014 ", v.description), v.isCurrent && /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, " [CURRENT]"));
140
+ })));
141
+ }
142
+ const scrollStart = Math.max(0, Math.min(
143
+ focusPos - Math.floor(maxVisible / 2),
144
+ selectableIndices.length - maxVisible
145
+ ));
146
+ const visibleSelectableRange = selectableIndices.slice(scrollStart, scrollStart + maxVisible);
147
+ const minVisibleEntry = visibleSelectableRange[0] ?? 0;
148
+ const maxVisibleEntry = visibleSelectableRange[visibleSelectableRange.length - 1] ?? entries.length - 1;
149
+ let renderStart = minVisibleEntry;
150
+ for (let i = minVisibleEntry - 1; i >= 0; i--) {
151
+ if (entries[i].type === "header") {
152
+ renderStart = i;
153
+ break;
154
+ }
155
+ }
156
+ const visibleEntries = entries.slice(renderStart, maxVisibleEntry + 1);
157
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: theme.accent, bold: true }, "Select Cloud Model"), /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "Enter to select \u25B8/\u2192 for variants Esc to cancel"), /* @__PURE__ */ React.createElement(Box, { marginTop: 1, flexDirection: "column" }, visibleEntries.map((entry, renderIdx) => {
158
+ const actualIdx = renderStart + renderIdx;
159
+ if (entry.type === "header") {
160
+ return /* @__PURE__ */ React.createElement(Box, { key: actualIdx, marginTop: actualIdx > 0 ? 1 : 0 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow", bold: true }, "\u2500\u2500 ", entry.label, " \u2500\u2500"));
161
+ }
162
+ const isFocused = selectableIndices[focusPos] === actualIdx;
163
+ const isGroup = entry.type === "group";
164
+ return /* @__PURE__ */ React.createElement(Box, { key: actualIdx }, /* @__PURE__ */ React.createElement(Text, { color: isFocused ? theme.accent : "#666666" }, isFocused ? "\u276F " : " "), /* @__PURE__ */ React.createElement(Text, { color: isFocused ? theme.accent : void 0, bold: isFocused }, entry.label), isGroup && /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " \u25B8"), /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, " \u2014 ", entry.description), entry.isCurrent && /* @__PURE__ */ React.createElement(Text, { color: "green", bold: true }, " [CURRENT]"));
165
+ }), scrollStart > 0 && /* @__PURE__ */ React.createElement(Text, { color: "#555555", dimColor: true }, " \u2191 more above"), scrollStart + maxVisible < selectableIndices.length && /* @__PURE__ */ React.createElement(Text, { color: "#555555", dimColor: true }, " \u2193 more below")));
166
+ };
167
+ export {
168
+ ModelPicker
169
+ };
170
+ //# sourceMappingURL=ModelPicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/components/ModelPicker.tsx"],"sourcesContent":["import React, { useState, useMemo } from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport { useTheme } from '../theme.js';\r\nimport type { ModelConfig } from '../../services/api-client.js';\r\n\r\n/**\r\n * Provider display names and ordering\r\n */\r\nconst PROVIDER_META: Record<string, { displayName: string; order: number }> = {\r\n google: { displayName: 'Google', order: 0 },\r\n anthropic: { displayName: 'Anthropic', order: 1 },\r\n bedrock: { displayName: 'Amazon Bedrock', order: 2 },\r\n maas: { displayName: 'MaaS', order: 3 },\r\n nvidia_nim: { displayName: 'NVIDIA NIM', order: 4 },\r\n silicon_flow: { displayName: 'Silicon Flow', order: 5 },\r\n openrouter: { displayName: 'OpenRouter', order: 6 },\r\n};\r\n\r\nfunction getProviderDisplay(provider: string): string {\r\n return PROVIDER_META[provider]?.displayName || provider;\r\n}\r\n\r\nfunction getProviderOrder(provider: string): number {\r\n return PROVIDER_META[provider]?.order ?? 99;\r\n}\r\n\r\n/**\r\n * A single entry in the flat picker list.\r\n * - header: non-selectable provider section title\r\n * - model: a standalone model\r\n * - group: a model that has multiple config variants (e.g. Gemini 3.1 Pro)\r\n */\r\ninterface PickerEntry {\r\n type: 'header' | 'model' | 'group';\r\n label: string;\r\n description?: string;\r\n isCurrent: boolean;\r\n /** Index in the original ModelConfig[] (for model entries) */\r\n modelIndex?: number;\r\n /** Group key (for group entries) */\r\n groupKey?: string;\r\n /** The variants belonging to this group */\r\n variants?: Array<{\r\n label: string;\r\n description: string;\r\n modelIndex: number;\r\n isCurrent: boolean;\r\n }>;\r\n}\r\n\r\ninterface ModelPickerProps {\r\n models: ModelConfig[];\r\n currentModelName: string;\r\n onSelect: (modelIndex: string) => void;\r\n onCancel: () => void;\r\n}\r\n\r\n/**\r\n * Get the display provider key for UI grouping.\r\n * Uses `displayProvider` if set (for models branded differently from their actual provider),\r\n * otherwise falls back to `provider`.\r\n */\r\nfunction getDisplayProviderKey(m: ModelConfig): string {\r\n return m.displayProvider || m.provider;\r\n}\r\n\r\n/**\r\n * Build the flat list of entries from model configs,\r\n * grouped by provider, with variant groups collapsed.\r\n */\r\nfunction buildEntries(models: ModelConfig[], currentModelName: string): PickerEntry[] {\r\n // Group models by display provider\r\n const byProvider = new Map<string, ModelConfig[]>();\r\n for (const m of models) {\r\n const key = getDisplayProviderKey(m);\r\n const list = byProvider.get(key) || [];\r\n list.push(m);\r\n byProvider.set(key, list);\r\n }\r\n\r\n // Sort providers\r\n const sortedProviders = Array.from(byProvider.keys()).sort(\r\n (a, b) => getProviderOrder(a) - getProviderOrder(b)\r\n );\r\n\r\n const entries: PickerEntry[] = [];\r\n\r\n for (const provider of sortedProviders) {\r\n const providerModels = byProvider.get(provider)!;\r\n\r\n // Add provider header\r\n entries.push({\r\n type: 'header',\r\n label: getProviderDisplay(provider),\r\n isCurrent: false,\r\n });\r\n\r\n // Group models with `group` field\r\n const grouped = new Map<string, ModelConfig[]>();\r\n const standalone: ModelConfig[] = [];\r\n\r\n for (const m of providerModels) {\r\n if (m.group) {\r\n const list = grouped.get(m.group) || [];\r\n list.push(m);\r\n grouped.set(m.group, list);\r\n } else {\r\n standalone.push(m);\r\n }\r\n }\r\n\r\n // Add grouped models (collapsed)\r\n for (const [groupKey, groupModels] of grouped) {\r\n // Use the first model's name to derive the group display name\r\n // Strip variant suffixes like \"(High)\", \"(Low)\" from the name\r\n const baseName = groupModels[0].name.replace(/\\s*\\(.*?\\)\\s*$/, '');\r\n const isAnyCurrent = groupModels.some(m => m.name === currentModelName);\r\n\r\n entries.push({\r\n type: 'group',\r\n label: baseName,\r\n description: groupModels[0].description.replace(/\\s*\\(.*?\\)\\s*$/, ''),\r\n isCurrent: isAnyCurrent,\r\n groupKey,\r\n variants: groupModels.map(m => ({\r\n label: m.variantLabel || m.name,\r\n description: m.description,\r\n modelIndex: models.indexOf(m),\r\n isCurrent: m.name === currentModelName,\r\n })),\r\n });\r\n }\r\n\r\n // Add standalone models\r\n for (const m of standalone) {\r\n entries.push({\r\n type: 'model',\r\n label: m.name,\r\n description: m.description,\r\n isCurrent: m.name === currentModelName,\r\n modelIndex: models.indexOf(m),\r\n });\r\n }\r\n }\r\n\r\n return entries;\r\n}\r\n\r\n/**\r\n * ModelPicker — a custom model selection screen with:\r\n * - Provider section headers (non-selectable)\r\n * - Variant groups that expand into a sub-picker on selection\r\n */\r\nexport const ModelPicker: React.FC<ModelPickerProps> = ({ models, currentModelName, onSelect, onCancel }) => {\r\n const theme = useTheme();\r\n\r\n // Build the flat entry list\r\n const entries = useMemo(() => buildEntries(models, currentModelName), [models, currentModelName]);\r\n\r\n // Selectable indices (skip headers)\r\n const selectableIndices = useMemo(\r\n () => entries.map((e, i) => (e.type !== 'header' ? i : -1)).filter(i => i >= 0),\r\n [entries]\r\n );\r\n\r\n // Phase: 'main' for model list, 'variant' for sub-selection\r\n const [phase, setPhase] = useState<'main' | 'variant'>('main');\r\n const [focusPos, setFocusPos] = useState(0); // index into selectableIndices\r\n const [activeGroup, setActiveGroup] = useState<PickerEntry | null>(null);\r\n const [variantFocusPos, setVariantFocusPos] = useState(0);\r\n\r\n // Scroll offset for main list\r\n const maxVisible = 20;\r\n\r\n useInput((input, key) => {\r\n if (phase === 'main') {\r\n if (key.upArrow) {\r\n setFocusPos(p => Math.max(0, p - 1));\r\n } else if (key.downArrow) {\r\n setFocusPos(p => Math.min(selectableIndices.length - 1, p + 1));\r\n } else if (key.return) {\r\n const entryIdx = selectableIndices[focusPos];\r\n const entry = entries[entryIdx];\r\n if (entry.type === 'group' && entry.variants && entry.variants.length > 1) {\r\n // Expand to variant picker\r\n setActiveGroup(entry);\r\n setVariantFocusPos(0);\r\n setPhase('variant');\r\n } else if (entry.type === 'group' && entry.variants && entry.variants.length === 1) {\r\n // Single variant - select directly\r\n onSelect(`${entry.variants[0].modelIndex}`);\r\n } else if (entry.type === 'model' && entry.modelIndex !== undefined) {\r\n onSelect(`${entry.modelIndex}`);\r\n }\r\n } else if (key.rightArrow) {\r\n // Right arrow opens variant picker for groups (same as Enter for groups)\r\n const entryIdx = selectableIndices[focusPos];\r\n const entry = entries[entryIdx];\r\n if (entry.type === 'group' && entry.variants && entry.variants.length > 1) {\r\n setActiveGroup(entry);\r\n setVariantFocusPos(0);\r\n setPhase('variant');\r\n }\r\n } else if (key.escape) {\r\n // Escape dismisses the picker without changing the model\r\n onCancel();\r\n }\r\n } else if (phase === 'variant' && activeGroup?.variants) {\r\n if (key.upArrow) {\r\n setVariantFocusPos(p => Math.max(0, p - 1));\r\n } else if (key.downArrow) {\r\n setVariantFocusPos(p => Math.min(activeGroup.variants!.length - 1, p + 1));\r\n } else if (key.return) {\r\n const variant = activeGroup.variants[variantFocusPos];\r\n onSelect(`${variant.modelIndex}`);\r\n } else if (key.escape || (key.leftArrow)) {\r\n // Go back to main list\r\n setPhase('main');\r\n setActiveGroup(null);\r\n }\r\n }\r\n });\r\n\r\n // ── Variant sub-picker ──\r\n if (phase === 'variant' && activeGroup?.variants) {\r\n return (\r\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor={theme.accent} paddingX={1}>\r\n <Text color={theme.accent} bold>Select Configuration for {activeGroup.label}</Text>\r\n <Text color=\"#666666\" dimColor>← Press Esc or Left Arrow to go back</Text>\r\n <Box marginTop={1} flexDirection=\"column\">\r\n {activeGroup.variants.map((v, i) => {\r\n const isFocused = variantFocusPos === i;\r\n return (\r\n <Box key={i}>\r\n <Text color={isFocused ? theme.accent : '#666666'}>{isFocused ? '❯ ' : ' '}</Text>\r\n <Text color={isFocused ? theme.accent : undefined} bold={isFocused}>\r\n {v.label}\r\n </Text>\r\n <Text color=\"#666666\"> — {v.description}</Text>\r\n {v.isCurrent && <Text color=\"green\" bold> [CURRENT]</Text>}\r\n </Box>\r\n );\r\n })}\r\n </Box>\r\n </Box>\r\n );\r\n }\r\n\r\n // ── Main picker ──\r\n // Compute scroll window\r\n const scrollStart = Math.max(0, Math.min(\r\n focusPos - Math.floor(maxVisible / 2),\r\n selectableIndices.length - maxVisible\r\n ));\r\n const visibleSelectableRange = selectableIndices.slice(scrollStart, scrollStart + maxVisible);\r\n\r\n // Determine which raw entries to show (include headers that fall between visible items)\r\n const minVisibleEntry = visibleSelectableRange[0] ?? 0;\r\n const maxVisibleEntry = visibleSelectableRange[visibleSelectableRange.length - 1] ?? entries.length - 1;\r\n\r\n // Walk backwards from minVisibleEntry to find the header for the first visible group\r\n let renderStart = minVisibleEntry;\r\n for (let i = minVisibleEntry - 1; i >= 0; i--) {\r\n if (entries[i].type === 'header') {\r\n renderStart = i;\r\n break;\r\n }\r\n }\r\n\r\n const visibleEntries = entries.slice(renderStart, maxVisibleEntry + 1);\r\n\r\n return (\r\n <Box flexDirection=\"column\" borderStyle=\"round\" borderColor={theme.accent} paddingX={1}>\r\n <Text color={theme.accent} bold>Select Cloud Model</Text>\r\n <Text color=\"#666666\" dimColor>Enter to select ▸/→ for variants Esc to cancel</Text>\r\n <Box marginTop={1} flexDirection=\"column\">\r\n {visibleEntries.map((entry, renderIdx) => {\r\n const actualIdx = renderStart + renderIdx;\r\n\r\n if (entry.type === 'header') {\r\n return (\r\n <Box key={actualIdx} marginTop={actualIdx > 0 ? 1 : 0}>\r\n <Text color=\"yellow\" bold>── {entry.label} ──</Text>\r\n </Box>\r\n );\r\n }\r\n\r\n const isFocused = selectableIndices[focusPos] === actualIdx;\r\n const isGroup = entry.type === 'group';\r\n\r\n return (\r\n <Box key={actualIdx}>\r\n <Text color={isFocused ? theme.accent : '#666666'}>{isFocused ? '❯ ' : ' '}</Text>\r\n <Text color={isFocused ? theme.accent : undefined} bold={isFocused}>\r\n {entry.label}\r\n </Text>\r\n {isGroup && <Text color=\"#666666\"> ▸</Text>}\r\n <Text color=\"#666666\"> — {entry.description}</Text>\r\n {entry.isCurrent && <Text color=\"green\" bold> [CURRENT]</Text>}\r\n </Box>\r\n );\r\n })}\r\n\r\n {/* Scroll indicators */}\r\n {scrollStart > 0 && (\r\n <Text color=\"#555555\" dimColor> ↑ more above</Text>\r\n )}\r\n {scrollStart + maxVisible < selectableIndices.length && (\r\n <Text color=\"#555555\" dimColor> ↓ more below</Text>\r\n )}\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n"],"mappings":"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","names":[]}
@@ -1,7 +1,9 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import Spinner from "ink-spinner";
4
+ import { useTheme } from "../theme.js";
4
5
  const MonitorMessageItem = ({ message }) => {
6
+ const theme = useTheme();
5
7
  switch (message.type) {
6
8
  case "thinking":
7
9
  if (message.isAnswered) {
@@ -9,7 +11,7 @@ const MonitorMessageItem = ({ message }) => {
9
11
  }
10
12
  return /* @__PURE__ */ React.createElement(Box, { marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#ffaa00" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, { color: "#ffaa00", dimColor: true }, " ", message.content));
11
13
  case "tool_call":
12
- return /* @__PURE__ */ React.createElement(Box, { marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff" }, "\u26A1 "), /* @__PURE__ */ React.createElement(Text, { color: "#00ccff" }, message.content));
14
+ return /* @__PURE__ */ React.createElement(Box, { marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: theme.accent }, "\u26A1 "), /* @__PURE__ */ React.createElement(Text, { color: theme.accent }, message.content));
13
15
  case "input_sent":
14
16
  return /* @__PURE__ */ React.createElement(Box, { marginY: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, "\u2192 "), /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, message.content));
15
17
  case "text":
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/MonitorModeAIPanel.tsx"],"sourcesContent":["/**\r\n * Monitor Mode AI Panel Component\r\n * \r\n * Displays AI monitoring messages in the lower half of the screen during\r\n * agent-controlled shell mode. Shows thinking indicators, tool calls,\r\n * and text outputs.\r\n */\r\n\r\nimport React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport Spinner from 'ink-spinner';\r\nimport { MonitorMessage } from '../../services/monitored-shell-manager.js';\r\n\r\nexport interface MonitorModeAIPanelProps {\r\n messages: MonitorMessage[];\r\n maxHeight: number; // Maximum height in lines\r\n isActive: boolean;\r\n}\r\n\r\n/**\r\n * Render a single monitoring message\r\n */\r\nconst MonitorMessageItem: React.FC<{ message: MonitorMessage }> = ({ message }) => {\r\n switch (message.type) {\r\n case 'thinking':\r\n // Show tick if answered, spinner if still waiting\r\n if (message.isAnswered) {\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#00cc66\">✓ </Text>\r\n <Text color=\"#00cc66\">{message.content}</Text>\r\n </Box>\r\n );\r\n }\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#ffaa00\">\r\n <Spinner type=\"dots\" />\r\n </Text>\r\n <Text color=\"#ffaa00\" dimColor> {message.content}</Text>\r\n </Box>\r\n );\r\n\r\n case 'tool_call':\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#00ccff\">⚡ </Text>\r\n <Text color=\"#00ccff\">{message.content}</Text>\r\n </Box>\r\n );\r\n\r\n case 'input_sent':\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#00cc66\">→ </Text>\r\n <Text color=\"#00cc66\">{message.content}</Text>\r\n </Box>\r\n );\r\n\r\n case 'text':\r\n default:\r\n return (\r\n <Box marginY={0}>\r\n <Text>{message.content}</Text>\r\n </Box>\r\n );\r\n }\r\n};\r\n\r\n/**\r\n * Monitor Mode AI Panel\r\n * \r\n * Displays in the lower half of focus mode when agent control is enabled.\r\n * Shows only the messages that fit within the available height.\r\n */\r\nexport const MonitorModeAIPanel: React.FC<MonitorModeAIPanelProps> = ({\r\n messages,\r\n maxHeight,\r\n isActive\r\n}) => {\r\n if (!isActive) {\r\n return null;\r\n }\r\n\r\n // Calculate how many messages to show (estimate 1-2 lines per message)\r\n // Keep only the most recent messages that fit\r\n const linesPerMessage = 2;\r\n const maxMessages = Math.floor(maxHeight / linesPerMessage);\r\n const visibleMessages = messages.slice(-maxMessages);\r\n\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n borderStyle=\"round\"\r\n borderColor=\"#9945FF\"\r\n paddingX={1}\r\n height={maxHeight}\r\n >\r\n {/* Header */}\r\n <Box marginBottom={1}>\r\n <Text color=\"#9945FF\" bold>\r\n <Spinner type=\"dots\" /> Agent Monitor\r\n </Text>\r\n <Text color=\"#666666\" dimColor> - AI is controlling shell input</Text>\r\n </Box>\r\n\r\n {/* Messages */}\r\n <Box flexDirection=\"column\" flexGrow={1}>\r\n {visibleMessages.length === 0 ? (\r\n <Text color=\"#666666\" dimColor>Monitoring for input prompts...</Text>\r\n ) : (\r\n visibleMessages.map((msg) => (\r\n <MonitorMessageItem key={msg.id} message={msg} />\r\n ))\r\n )}\r\n </Box>\r\n\r\n {/* Footer hint */}\r\n <Box marginTop={1}>\r\n <Text color=\"#666666\" dimColor>\r\n Alt+I to disable agent control\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n\r\nexport default MonitorModeAIPanel;\r\n"],"mappings":"AAQA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,OAAO,aAAa;AAYpB,MAAM,qBAA4D,CAAC,EAAE,QAAQ,MAAM;AAC/E,UAAQ,QAAQ,MAAM;AAAA,IAClB,KAAK;AAED,UAAI,QAAQ,YAAY;AACpB,eACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aAAU,SAAE,GACxB,oCAAC,QAAK,OAAM,aAAW,QAAQ,OAAQ,CAC3C;AAAA,MAER;AACA,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aACR,oCAAC,WAAQ,MAAK,QAAO,CACzB,GACA,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,KAAE,QAAQ,OAAQ,CACrD;AAAA,IAGR,KAAK;AACD,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aAAU,SAAE,GACxB,oCAAC,QAAK,OAAM,aAAW,QAAQ,OAAQ,CAC3C;AAAA,IAGR,KAAK;AACD,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aAAU,SAAE,GACxB,oCAAC,QAAK,OAAM,aAAW,QAAQ,OAAQ,CAC3C;AAAA,IAGR,KAAK;AAAA,IACL;AACI,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,YAAM,QAAQ,OAAQ,CAC3B;AAAA,EAEZ;AACJ;AAQO,MAAM,qBAAwD,CAAC;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AACJ,MAAM;AACF,MAAI,CAAC,UAAU;AACX,WAAO;AAAA,EACX;AAIA,QAAM,kBAAkB;AACxB,QAAM,cAAc,KAAK,MAAM,YAAY,eAAe;AAC1D,QAAM,kBAAkB,SAAS,MAAM,CAAC,WAAW;AAEnD,SACI;AAAA,IAAC;AAAA;AAAA,MACG,eAAc;AAAA,MACd,aAAY;AAAA,MACZ,aAAY;AAAA,MACZ,UAAU;AAAA,MACV,QAAQ;AAAA;AAAA,IAGR,oCAAC,OAAI,cAAc,KACf,oCAAC,QAAK,OAAM,WAAU,MAAI,QACtB,oCAAC,WAAQ,MAAK,QAAO,GAAE,gBAC3B,GACA,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,kCAAgC,CACnE;AAAA,IAGA,oCAAC,OAAI,eAAc,UAAS,UAAU,KACjC,gBAAgB,WAAW,IACxB,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,iCAA+B,IAE9D,gBAAgB,IAAI,CAAC,QACjB,oCAAC,sBAAmB,KAAK,IAAI,IAAI,SAAS,KAAK,CAClD,CAET;AAAA,IAGA,oCAAC,OAAI,WAAW,KACZ,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,gCAE/B,CACJ;AAAA,EACJ;AAER;AAEA,IAAO,6BAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/MonitorModeAIPanel.tsx"],"sourcesContent":["/**\r\n * Monitor Mode AI Panel Component\r\n * \r\n * Displays AI monitoring messages in the lower half of the screen during\r\n * agent-controlled shell mode. Shows thinking indicators, tool calls,\r\n * and text outputs.\r\n */\r\n\r\nimport React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport Spinner from 'ink-spinner';\r\nimport { MonitorMessage } from '../../services/monitored-shell-manager.js';\r\nimport { useTheme } from '../theme.js';\r\n\r\nexport interface MonitorModeAIPanelProps {\r\n messages: MonitorMessage[];\r\n maxHeight: number; // Maximum height in lines\r\n isActive: boolean;\r\n}\r\n\r\n/**\r\n * Render a single monitoring message\r\n */\r\nconst MonitorMessageItem: React.FC<{ message: MonitorMessage }> = ({ message }) => {\r\n const theme = useTheme();\r\n switch (message.type) {\r\n case 'thinking':\r\n // Show tick if answered, spinner if still waiting\r\n if (message.isAnswered) {\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#00cc66\">✓ </Text>\r\n <Text color=\"#00cc66\">{message.content}</Text>\r\n </Box>\r\n );\r\n }\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#ffaa00\">\r\n <Spinner type=\"dots\" />\r\n </Text>\r\n <Text color=\"#ffaa00\" dimColor> {message.content}</Text>\r\n </Box>\r\n );\r\n\r\n case 'tool_call':\r\n return (\r\n <Box marginY={0}>\r\n <Text color={theme.accent}>⚡ </Text>\r\n <Text color={theme.accent}>{message.content}</Text>\r\n </Box>\r\n );\r\n\r\n case 'input_sent':\r\n return (\r\n <Box marginY={0}>\r\n <Text color=\"#00cc66\">→ </Text>\r\n <Text color=\"#00cc66\">{message.content}</Text>\r\n </Box>\r\n );\r\n\r\n case 'text':\r\n default:\r\n return (\r\n <Box marginY={0}>\r\n <Text>{message.content}</Text>\r\n </Box>\r\n );\r\n }\r\n};\r\n\r\n/**\r\n * Monitor Mode AI Panel\r\n * \r\n * Displays in the lower half of focus mode when agent control is enabled.\r\n * Shows only the messages that fit within the available height.\r\n */\r\nexport const MonitorModeAIPanel: React.FC<MonitorModeAIPanelProps> = ({\r\n messages,\r\n maxHeight,\r\n isActive\r\n}) => {\r\n if (!isActive) {\r\n return null;\r\n }\r\n\r\n // Calculate how many messages to show (estimate 1-2 lines per message)\r\n // Keep only the most recent messages that fit\r\n const linesPerMessage = 2;\r\n const maxMessages = Math.floor(maxHeight / linesPerMessage);\r\n const visibleMessages = messages.slice(-maxMessages);\r\n\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n borderStyle=\"round\"\r\n borderColor=\"#9945FF\"\r\n paddingX={1}\r\n height={maxHeight}\r\n >\r\n {/* Header */}\r\n <Box marginBottom={1}>\r\n <Text color=\"#9945FF\" bold>\r\n <Spinner type=\"dots\" /> Agent Monitor\r\n </Text>\r\n <Text color=\"#666666\" dimColor> - AI is controlling shell input</Text>\r\n </Box>\r\n\r\n {/* Messages */}\r\n <Box flexDirection=\"column\" flexGrow={1}>\r\n {visibleMessages.length === 0 ? (\r\n <Text color=\"#666666\" dimColor>Monitoring for input prompts...</Text>\r\n ) : (\r\n visibleMessages.map((msg) => (\r\n <MonitorMessageItem key={msg.id} message={msg} />\r\n ))\r\n )}\r\n </Box>\r\n\r\n {/* Footer hint */}\r\n <Box marginTop={1}>\r\n <Text color=\"#666666\" dimColor>\r\n Alt+I to disable agent control\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n\r\nexport default MonitorModeAIPanel;\r\n"],"mappings":"AAQA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,OAAO,aAAa;AAEpB,SAAS,gBAAgB;AAWzB,MAAM,qBAA4D,CAAC,EAAE,QAAQ,MAAM;AAC/E,QAAM,QAAQ,SAAS;AACvB,UAAQ,QAAQ,MAAM;AAAA,IAClB,KAAK;AAED,UAAI,QAAQ,YAAY;AACpB,eACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aAAU,SAAE,GACxB,oCAAC,QAAK,OAAM,aAAW,QAAQ,OAAQ,CAC3C;AAAA,MAER;AACA,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aACR,oCAAC,WAAQ,MAAK,QAAO,CACzB,GACA,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,KAAE,QAAQ,OAAQ,CACrD;AAAA,IAGR,KAAK;AACD,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAO,MAAM,UAAQ,SAAE,GAC7B,oCAAC,QAAK,OAAO,MAAM,UAAS,QAAQ,OAAQ,CAChD;AAAA,IAGR,KAAK;AACD,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,QAAK,OAAM,aAAU,SAAE,GACxB,oCAAC,QAAK,OAAM,aAAW,QAAQ,OAAQ,CAC3C;AAAA,IAGR,KAAK;AAAA,IACL;AACI,aACI,oCAAC,OAAI,SAAS,KACV,oCAAC,YAAM,QAAQ,OAAQ,CAC3B;AAAA,EAEZ;AACJ;AAQO,MAAM,qBAAwD,CAAC;AAAA,EAClE;AAAA,EACA;AAAA,EACA;AACJ,MAAM;AACF,MAAI,CAAC,UAAU;AACX,WAAO;AAAA,EACX;AAIA,QAAM,kBAAkB;AACxB,QAAM,cAAc,KAAK,MAAM,YAAY,eAAe;AAC1D,QAAM,kBAAkB,SAAS,MAAM,CAAC,WAAW;AAEnD,SACI;AAAA,IAAC;AAAA;AAAA,MACG,eAAc;AAAA,MACd,aAAY;AAAA,MACZ,aAAY;AAAA,MACZ,UAAU;AAAA,MACV,QAAQ;AAAA;AAAA,IAGR,oCAAC,OAAI,cAAc,KACf,oCAAC,QAAK,OAAM,WAAU,MAAI,QACtB,oCAAC,WAAQ,MAAK,QAAO,GAAE,gBAC3B,GACA,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,kCAAgC,CACnE;AAAA,IAGA,oCAAC,OAAI,eAAc,UAAS,UAAU,KACjC,gBAAgB,WAAW,IACxB,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,iCAA+B,IAE9D,gBAAgB,IAAI,CAAC,QACjB,oCAAC,sBAAmB,KAAK,IAAI,IAAI,SAAS,KAAK,CAClD,CAET;AAAA,IAGA,oCAAC,OAAI,WAAW,KACZ,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,gCAE/B,CACJ;AAAA,EACJ;AAER;AAEA,IAAO,6BAAQ;","names":[]}
@@ -1,24 +1,30 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
+ import { useTheme } from "../theme.js";
3
4
  const PlanAcceptedMessage = ({
4
5
  planTitle,
5
- totalTasks,
6
- tasks
6
+ totalSteps,
7
+ steps
7
8
  }) => {
9
+ const theme = useTheme();
8
10
  return /* @__PURE__ */ React.createElement(
9
11
  Box,
10
12
  {
11
13
  flexDirection: "column",
12
14
  borderStyle: "round",
13
- borderColor: "#00cc66",
15
+ borderColor: theme.accent,
14
16
  paddingX: 1,
15
17
  paddingY: 0,
16
18
  marginY: 0,
17
19
  alignSelf: "flex-start"
18
20
  },
19
- /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#00cc66", bold: true }, "\u2713 "), /* @__PURE__ */ React.createElement(Text, { color: "#00cc66", bold: true }, "Plan Accepted"), planTitle && /* @__PURE__ */ React.createElement(Text, { color: "#888888" }, " - ", planTitle)),
20
- tasks && tasks.length > 0 && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 0, marginLeft: 2 }, tasks.map((task, taskIndex) => /* @__PURE__ */ React.createElement(Box, { key: taskIndex, flexDirection: "column" }, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#00ccff" }, "[ ] "), /* @__PURE__ */ React.createElement(Text, { color: "#cccccc", wrap: "wrap" }, taskIndex + 1, ". ", task.description)), task.subtasks && task.subtasks.map((subtask, subIndex) => /* @__PURE__ */ React.createElement(Box, { key: subIndex, marginLeft: 3 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666" }, "[ ] "), /* @__PURE__ */ React.createElement(Text, { color: "#888888", wrap: "wrap" }, taskIndex + 1, ".", subIndex + 1, ". ", subtask.description)))))),
21
- /* @__PURE__ */ React.createElement(Box, { marginTop: 0, marginLeft: 2 }, /* @__PURE__ */ React.createElement(Text, { color: "#666666", dimColor: true }, "Starting execution of ", totalTasks || 0, " task", (totalTasks || 0) > 1 ? "s" : "", "..."))
21
+ /* @__PURE__ */ React.createElement(Box, { marginBottom: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#00cc66", bold: true }, "\u2713", " Plan Accepted"), planTitle && /* @__PURE__ */ React.createElement(Text, { color: "#888888" }, " ", "\u2014", " ", planTitle)),
22
+ steps && steps.length > 0 && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 0 }, steps.map((step, index) => {
23
+ const isFirst = index === 0;
24
+ const marker = isFirst ? /* @__PURE__ */ React.createElement(Text, { color: "#f59e0b" }, "\u25D4", " ") : /* @__PURE__ */ React.createElement(Text, { color: "#555555" }, " ");
25
+ const textColor = isFirst ? "#f59e0b" : "#555555";
26
+ return /* @__PURE__ */ React.createElement(Box, { key: step.id }, marker, /* @__PURE__ */ React.createElement(Text, { color: textColor, wrap: "truncate-end" }, step.id, ". ", step.description));
27
+ }))
22
28
  );
23
29
  };
24
30
  var PlanAcceptedMessage_default = PlanAcceptedMessage;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/components/PlanAcceptedMessage.tsx"],"sourcesContent":["/**\r\n * PlanAcceptedMessage Component\r\n * Displays a message when the plan is accepted and execution begins\r\n * Shows all tasks and subtasks as a static reference\r\n */\r\n\r\nimport React from 'react';\r\nimport { Box, Text } from 'ink';\r\n\r\ninterface TaskItem {\r\n description: string;\r\n subtasks?: Array<{ description: string }>;\r\n}\r\n\r\ninterface PlanAcceptedMessageProps {\r\n planTitle?: string;\r\n totalTasks?: number;\r\n tasks?: TaskItem[];\r\n}\r\n\r\nexport const PlanAcceptedMessage: React.FC<PlanAcceptedMessageProps> = ({\r\n planTitle,\r\n totalTasks,\r\n tasks\r\n}) => {\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n borderStyle=\"round\"\r\n borderColor=\"#00cc66\"\r\n paddingX={1}\r\n paddingY={0}\r\n marginY={0}\r\n alignSelf=\"flex-start\"\r\n >\r\n {/* Header */}\r\n <Box>\r\n <Text color=\"#00cc66\" bold>✓ </Text>\r\n <Text color=\"#00cc66\" bold>Plan Accepted</Text>\r\n {planTitle && (\r\n <Text color=\"#888888\"> - {planTitle}</Text>\r\n )}\r\n </Box>\r\n\r\n {/* Task List */}\r\n {tasks && tasks.length > 0 && (\r\n <Box flexDirection=\"column\" marginTop={0} marginLeft={2}>\r\n {tasks.map((task, taskIndex) => (\r\n <Box key={taskIndex} flexDirection=\"column\">\r\n {/* Main task */}\r\n <Box>\r\n <Text color=\"#00ccff\">[ ] </Text>\r\n <Text color=\"#cccccc\" wrap=\"wrap\">\r\n {taskIndex + 1}. {task.description}\r\n </Text>\r\n </Box>\r\n {/* Subtasks */}\r\n {task.subtasks && task.subtasks.map((subtask, subIndex) => (\r\n <Box key={subIndex} marginLeft={3}>\r\n <Text color=\"#666666\">[ ] </Text>\r\n <Text color=\"#888888\" wrap=\"wrap\">\r\n {taskIndex + 1}.{subIndex + 1}. {subtask.description}\r\n </Text>\r\n </Box>\r\n ))}\r\n </Box>\r\n ))}\r\n </Box>\r\n )}\r\n\r\n {/* Starting message */}\r\n <Box marginTop={0} marginLeft={2}>\r\n <Text color=\"#666666\" dimColor>\r\n Starting execution of {totalTasks || 0} task{(totalTasks || 0) > 1 ? 's' : ''}...\r\n </Text>\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n\r\nexport default PlanAcceptedMessage;\r\n"],"mappings":"AAMA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAanB,MAAM,sBAA0D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AACJ,MAAM;AACF,SACI;AAAA,IAAC;AAAA;AAAA,MACG,eAAc;AAAA,MACd,aAAY;AAAA,MACZ,aAAY;AAAA,MACZ,UAAU;AAAA,MACV,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAU;AAAA;AAAA,IAGV,oCAAC,WACG,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,SAAE,GAC7B,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,eAAa,GACvC,aACG,oCAAC,QAAK,OAAM,aAAU,OAAI,SAAU,CAE5C;AAAA,IAGC,SAAS,MAAM,SAAS,KACrB,oCAAC,OAAI,eAAc,UAAS,WAAW,GAAG,YAAY,KACjD,MAAM,IAAI,CAAC,MAAM,cACd,oCAAC,OAAI,KAAK,WAAW,eAAc,YAE/B,oCAAC,WACG,oCAAC,QAAK,OAAM,aAAU,MAAI,GAC1B,oCAAC,QAAK,OAAM,WAAU,MAAK,UACtB,YAAY,GAAE,MAAG,KAAK,WAC3B,CACJ,GAEC,KAAK,YAAY,KAAK,SAAS,IAAI,CAAC,SAAS,aAC1C,oCAAC,OAAI,KAAK,UAAU,YAAY,KAC5B,oCAAC,QAAK,OAAM,aAAU,MAAI,GAC1B,oCAAC,QAAK,OAAM,WAAU,MAAK,UACtB,YAAY,GAAE,KAAE,WAAW,GAAE,MAAG,QAAQ,WAC7C,CACJ,CACH,CACL,CACH,CACL;AAAA,IAIJ,oCAAC,OAAI,WAAW,GAAG,YAAY,KAC3B,oCAAC,QAAK,OAAM,WAAU,UAAQ,QAAC,0BACJ,cAAc,GAAE,UAAO,cAAc,KAAK,IAAI,MAAM,IAAG,KAClF,CACJ;AAAA,EACJ;AAER;AAEA,IAAO,8BAAQ;","names":[]}
1
+ {"version":3,"sources":["../../../src/ui/components/PlanAcceptedMessage.tsx"],"sourcesContent":["/**\r\n * PlanAcceptedMessage Component\r\n * Displays implementation steps list when the plan is accepted.\r\n * First step gets a yellow spinner (AI is working on it), rest are dim.\r\n * Wrapped in a bordered box for visual consistency in the chat.\r\n */\r\n\r\nimport React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface StepItem {\r\n id: number;\r\n description: string;\r\n status: 'pending' | 'completed';\r\n}\r\n\r\ninterface PlanAcceptedMessageProps {\r\n planTitle?: string;\r\n totalSteps?: number;\r\n steps?: StepItem[];\r\n}\r\n\r\nexport const PlanAcceptedMessage: React.FC<PlanAcceptedMessageProps> = ({\r\n planTitle,\r\n totalSteps,\r\n steps\r\n}) => {\r\n const theme = useTheme();\r\n\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n borderStyle=\"round\"\r\n borderColor={theme.accent}\r\n paddingX={1}\r\n paddingY={0}\r\n marginY={0}\r\n alignSelf=\"flex-start\"\r\n >\r\n {/* Header */}\r\n <Box marginBottom={0}>\r\n <Text color=\"#00cc66\" bold>{'\\u2713'} Plan Accepted</Text>\r\n {planTitle && (\r\n <Text color=\"#888888\"> {'\\u2014'} {planTitle}</Text>\r\n )}\r\n </Box>\r\n\r\n {/* Implementation Steps List */}\r\n {steps && steps.length > 0 && (\r\n <Box flexDirection=\"column\" marginTop={0}>\r\n {steps.map((step, index) => {\r\n const isFirst = index === 0;\r\n const marker = isFirst\r\n ? <Text color=\"#f59e0b\">{'\\u25D4'} </Text>\r\n : <Text color=\"#555555\"> </Text>;\r\n const textColor = isFirst ? '#f59e0b' : '#555555';\r\n\r\n return (\r\n <Box key={step.id}>\r\n {marker}\r\n <Text color={textColor} wrap=\"truncate-end\">\r\n {step.id}. {step.description}\r\n </Text>\r\n </Box>\r\n );\r\n })}\r\n </Box>\r\n )}\r\n </Box>\r\n );\r\n};\r\n\r\nexport default PlanAcceptedMessage;\r\n"],"mappings":"AAOA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,SAAS,gBAAgB;AAclB,MAAM,sBAA0D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AACJ,MAAM;AACF,QAAM,QAAQ,SAAS;AAEvB,SACI;AAAA,IAAC;AAAA;AAAA,MACG,eAAc;AAAA,MACd,aAAY;AAAA,MACZ,aAAa,MAAM;AAAA,MACnB,UAAU;AAAA,MACV,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAU;AAAA;AAAA,IAGV,oCAAC,OAAI,cAAc,KACf,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAE,UAAS,gBAAc,GAClD,aACG,oCAAC,QAAK,OAAM,aAAU,KAAE,UAAS,KAAE,SAAU,CAErD;AAAA,IAGC,SAAS,MAAM,SAAS,KACrB,oCAAC,OAAI,eAAc,UAAS,WAAW,KAClC,MAAM,IAAI,CAAC,MAAM,UAAU;AACxB,YAAM,UAAU,UAAU;AAC1B,YAAM,SAAS,UACT,oCAAC,QAAK,OAAM,aAAW,UAAS,GAAC,IACjC,oCAAC,QAAK,OAAM,aAAU,IAAE;AAC9B,YAAM,YAAY,UAAU,YAAY;AAExC,aACI,oCAAC,OAAI,KAAK,KAAK,MACV,QACD,oCAAC,QAAK,OAAO,WAAW,MAAK,kBACxB,KAAK,IAAG,MAAG,KAAK,WACrB,CACJ;AAAA,IAER,CAAC,CACL;AAAA,EAER;AAER;AAEA,IAAO,8BAAQ;","names":[]}
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { useTheme } from "../theme.js";
4
+ const PlanQuestionMessage = ({
5
+ question,
6
+ options,
7
+ userAnswer,
8
+ wasSkipped
9
+ }) => {
10
+ const theme = useTheme();
11
+ const selectedOptionIndex = options.findIndex((o) => o === userAnswer);
12
+ const isCustom = !wasSkipped && selectedOptionIndex === -1;
13
+ return /* @__PURE__ */ React.createElement(
14
+ Box,
15
+ {
16
+ flexDirection: "column",
17
+ borderStyle: "round",
18
+ borderColor: "#f59e0b",
19
+ paddingX: 1,
20
+ paddingY: 0,
21
+ marginY: 0,
22
+ alignSelf: "flex-start"
23
+ },
24
+ /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#f59e0b", bold: true }, "? "), /* @__PURE__ */ React.createElement(Text, { color: "#ffffff", wrap: "wrap" }, question)),
25
+ /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0 }, options.map((option, index) => {
26
+ const isSelected = index === selectedOptionIndex;
27
+ return /* @__PURE__ */ React.createElement(Box, { key: index }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "#00cc66" : "#555555" }, isSelected ? "\u2713 " : " "), /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "#00cc66" : "#555555" }, index + 1, ". ", option));
28
+ })),
29
+ /* @__PURE__ */ React.createElement(Box, { marginTop: 0, marginLeft: 1 }, wasSkipped ? /* @__PURE__ */ React.createElement(Text, { color: "#888888", italic: true }, "Skipped - AI will decide the best approach") : isCustom ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { color: "#00bbff", bold: true }, "Custom: "), /* @__PURE__ */ React.createElement(Text, { color: "#ffffff", wrap: "wrap" }, userAnswer)) : /* @__PURE__ */ React.createElement(Text, { color: "#00cc66" }, "Selected option ", selectedOptionIndex + 1))
30
+ );
31
+ };
32
+ var PlanQuestionMessage_default = PlanQuestionMessage;
33
+ export {
34
+ PlanQuestionMessage,
35
+ PlanQuestionMessage_default as default
36
+ };
37
+ //# sourceMappingURL=PlanQuestionMessage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/components/PlanQuestionMessage.tsx"],"sourcesContent":["/**\r\n * PlanQuestionMessage Component\r\n * \r\n * Static message displayed in chat history showing a clarifying question\r\n * the AI asked during plan mode and the user's answer.\r\n */\r\n\r\nimport React from 'react';\r\nimport { Box, Text } from 'ink';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface PlanQuestionMessageProps {\r\n question: string;\r\n options: string[];\r\n userAnswer: string;\r\n wasSkipped: boolean;\r\n}\r\n\r\nexport const PlanQuestionMessage: React.FC<PlanQuestionMessageProps> = ({\r\n question,\r\n options,\r\n userAnswer,\r\n wasSkipped\r\n}) => {\r\n const theme = useTheme();\r\n\r\n // Determine if the user picked one of the suggested options or typed custom\r\n const selectedOptionIndex = options.findIndex(o => o === userAnswer);\r\n const isCustom = !wasSkipped && selectedOptionIndex === -1;\r\n\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n borderStyle=\"round\"\r\n borderColor=\"#f59e0b\"\r\n paddingX={1}\r\n paddingY={0}\r\n marginY={0}\r\n alignSelf=\"flex-start\"\r\n >\r\n {/* Question */}\r\n <Box>\r\n <Text color=\"#f59e0b\" bold>? </Text>\r\n <Text color=\"#ffffff\" wrap=\"wrap\">{question}</Text>\r\n </Box>\r\n\r\n {/* Options with the selected one highlighted */}\r\n <Box flexDirection=\"column\" marginLeft={2} marginTop={0}>\r\n {options.map((option, index) => {\r\n const isSelected = index === selectedOptionIndex;\r\n return (\r\n <Box key={index}>\r\n <Text color={isSelected ? '#00cc66' : '#555555'}>\r\n {isSelected ? '\\u2713 ' : ' '}\r\n </Text>\r\n <Text color={isSelected ? '#00cc66' : '#555555'}>\r\n {index + 1}. {option}\r\n </Text>\r\n </Box>\r\n );\r\n })}\r\n </Box>\r\n\r\n {/* Answer line */}\r\n <Box marginTop={0} marginLeft={1}>\r\n {wasSkipped ? (\r\n <Text color=\"#888888\" italic>Skipped - AI will decide the best approach</Text>\r\n ) : isCustom ? (\r\n <>\r\n <Text color=\"#00bbff\" bold>Custom: </Text>\r\n <Text color=\"#ffffff\" wrap=\"wrap\">{userAnswer}</Text>\r\n </>\r\n ) : (\r\n <Text color=\"#00cc66\">Selected option {selectedOptionIndex + 1}</Text>\r\n )}\r\n </Box>\r\n </Box>\r\n );\r\n};\r\n\r\nexport default PlanQuestionMessage;\r\n"],"mappings":"AAOA,OAAO,WAAW;AAClB,SAAS,KAAK,YAAY;AAC1B,SAAS,gBAAgB;AASlB,MAAM,sBAA0D,CAAC;AAAA,EACpE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,MAAM;AACF,QAAM,QAAQ,SAAS;AAGvB,QAAM,sBAAsB,QAAQ,UAAU,OAAK,MAAM,UAAU;AACnE,QAAM,WAAW,CAAC,cAAc,wBAAwB;AAExD,SACI;AAAA,IAAC;AAAA;AAAA,MACG,eAAc;AAAA,MACd,aAAY;AAAA,MACZ,aAAY;AAAA,MACZ,UAAU;AAAA,MACV,UAAU;AAAA,MACV,SAAS;AAAA,MACT,WAAU;AAAA;AAAA,IAGV,oCAAC,WACG,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,IAAE,GAC7B,oCAAC,QAAK,OAAM,WAAU,MAAK,UAAQ,QAAS,CAChD;AAAA,IAGA,oCAAC,OAAI,eAAc,UAAS,YAAY,GAAG,WAAW,KACjD,QAAQ,IAAI,CAAC,QAAQ,UAAU;AAC5B,YAAM,aAAa,UAAU;AAC7B,aACI,oCAAC,OAAI,KAAK,SACN,oCAAC,QAAK,OAAO,aAAa,YAAY,aACjC,aAAa,YAAY,IAC9B,GACA,oCAAC,QAAK,OAAO,aAAa,YAAY,aACjC,QAAQ,GAAE,MAAG,MAClB,CACJ;AAAA,IAER,CAAC,CACL;AAAA,IAGA,oCAAC,OAAI,WAAW,GAAG,YAAY,KAC1B,aACG,oCAAC,QAAK,OAAM,WAAU,QAAM,QAAC,4CAA0C,IACvE,WACA,0DACI,oCAAC,QAAK,OAAM,WAAU,MAAI,QAAC,UAAQ,GACnC,oCAAC,QAAK,OAAM,WAAU,MAAK,UAAQ,UAAW,CAClD,IAEA,oCAAC,QAAK,OAAM,aAAU,oBAAiB,sBAAsB,CAAE,CAEvE;AAAA,EACJ;AAER;AAEA,IAAO,8BAAQ;","names":[]}
@@ -0,0 +1,138 @@
1
+ import React, { useState, useRef } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import { useTheme } from "../theme.js";
5
+ const PlanQuestionScreen = ({
6
+ question,
7
+ options,
8
+ onAnswer,
9
+ onSkip
10
+ }) => {
11
+ const theme = useTheme();
12
+ const allItems = [...options, "__custom__", "__skip__"];
13
+ const [selectedIndex, setSelectedIndex] = useState(0);
14
+ const [mode, setMode] = useState("selecting");
15
+ const [customValue, setCustomValue] = useState("");
16
+ const submittedRef = useRef(false);
17
+ useInput((input, key) => {
18
+ if (submittedRef.current) return;
19
+ if (mode === "selecting") {
20
+ if (key.upArrow) {
21
+ setSelectedIndex((prev) => (prev - 1 + allItems.length) % allItems.length);
22
+ return;
23
+ }
24
+ if (key.downArrow) {
25
+ setSelectedIndex((prev) => (prev + 1) % allItems.length);
26
+ return;
27
+ }
28
+ if (key.return) {
29
+ const item = allItems[selectedIndex];
30
+ if (item === "__skip__") {
31
+ submittedRef.current = true;
32
+ onSkip();
33
+ } else if (item === "__custom__") {
34
+ setMode("custom-input");
35
+ } else {
36
+ submittedRef.current = true;
37
+ onAnswer(item);
38
+ }
39
+ return;
40
+ }
41
+ const num = parseInt(input, 10);
42
+ if (!isNaN(num) && num >= 1 && num <= options.length) {
43
+ submittedRef.current = true;
44
+ onAnswer(options[num - 1]);
45
+ return;
46
+ }
47
+ if (input.toLowerCase() === "s") {
48
+ submittedRef.current = true;
49
+ onSkip();
50
+ return;
51
+ }
52
+ }
53
+ if (mode === "custom-input") {
54
+ if (key.escape) {
55
+ setMode("selecting");
56
+ setCustomValue("");
57
+ return;
58
+ }
59
+ }
60
+ });
61
+ const handleCustomSubmit = (value) => {
62
+ if (submittedRef.current) return;
63
+ const trimmed = value.trim();
64
+ if (trimmed.length > 0) {
65
+ submittedRef.current = true;
66
+ onAnswer(trimmed);
67
+ }
68
+ };
69
+ return /* @__PURE__ */ React.createElement(
70
+ Box,
71
+ {
72
+ flexDirection: "column",
73
+ borderStyle: "round",
74
+ borderColor: "#f59e0b",
75
+ paddingX: 1,
76
+ paddingY: 0,
77
+ marginY: 0
78
+ },
79
+ /* @__PURE__ */ React.createElement(Box, { marginBottom: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#f59e0b", bold: true }, "? "), /* @__PURE__ */ React.createElement(Text, { color: "#ffffff", bold: true, wrap: "wrap" }, question)),
80
+ mode === "selecting" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 0, marginLeft: 1 }, options.map((option, index) => {
81
+ const isSelected = selectedIndex === index;
82
+ return /* @__PURE__ */ React.createElement(Box, { key: index }, /* @__PURE__ */ React.createElement(Text, { color: isSelected ? theme.accent : "#666666", bold: isSelected }, isSelected ? "> " : " "), /* @__PURE__ */ React.createElement(Text, { color: isSelected ? "#ffffff" : "#999999", bold: isSelected }, index + 1, ". ", option));
83
+ }), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(
84
+ Text,
85
+ {
86
+ color: selectedIndex === options.length ? "#00bbff" : "#666666",
87
+ bold: selectedIndex === options.length
88
+ },
89
+ selectedIndex === options.length ? "> " : " "
90
+ ), /* @__PURE__ */ React.createElement(
91
+ Text,
92
+ {
93
+ color: selectedIndex === options.length ? "#00bbff" : "#555555",
94
+ bold: selectedIndex === options.length
95
+ },
96
+ "Custom answer..."
97
+ )), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(
98
+ Text,
99
+ {
100
+ color: selectedIndex === options.length + 1 ? "#888888" : "#666666",
101
+ bold: selectedIndex === options.length + 1
102
+ },
103
+ selectedIndex === options.length + 1 ? "> " : " "
104
+ ), /* @__PURE__ */ React.createElement(
105
+ Text,
106
+ {
107
+ color: selectedIndex === options.length + 1 ? "#888888" : "#444444",
108
+ bold: selectedIndex === options.length + 1,
109
+ italic: true
110
+ },
111
+ "Skip (let AI decide)"
112
+ )), /* @__PURE__ */ React.createElement(Box, { marginTop: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#555555", dimColor: true }, "Arrow keys to navigate, Enter to select, 1-", options.length, " for quick select, S to skip"))),
113
+ mode === "custom-input" && /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginTop: 0, marginLeft: 1 }, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "#00bbff", bold: true }, "Your answer: ")), /* @__PURE__ */ React.createElement(
114
+ Box,
115
+ {
116
+ borderStyle: "round",
117
+ borderColor: "#00bbff",
118
+ paddingX: 1,
119
+ paddingY: 0
120
+ },
121
+ /* @__PURE__ */ React.createElement(
122
+ TextInput,
123
+ {
124
+ value: customValue,
125
+ onChange: setCustomValue,
126
+ onSubmit: handleCustomSubmit,
127
+ placeholder: "Type your answer and press Enter..."
128
+ }
129
+ )
130
+ ), /* @__PURE__ */ React.createElement(Box, { marginTop: 0 }, /* @__PURE__ */ React.createElement(Text, { color: "#555555", dimColor: true }, "Enter to submit, ESC to go back to options")))
131
+ );
132
+ };
133
+ var PlanQuestionScreen_default = PlanQuestionScreen;
134
+ export {
135
+ PlanQuestionScreen,
136
+ PlanQuestionScreen_default as default
137
+ };
138
+ //# sourceMappingURL=PlanQuestionScreen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/ui/components/PlanQuestionScreen.tsx"],"sourcesContent":["/**\r\n * PlanQuestionScreen Component\r\n * \r\n * Displayed when the AI asks a clarifying question during plan mode.\r\n * Shows the question with selectable options, a custom answer input, and a skip button.\r\n * \r\n * States:\r\n * - 'selecting': User is navigating options with arrow keys\r\n * - 'custom-input': User selected \"Custom answer\" and is typing\r\n */\r\n\r\nimport React, { useState, useRef } from 'react';\r\nimport { Box, Text, useInput } from 'ink';\r\nimport TextInput from 'ink-text-input';\r\nimport { useTheme } from '../theme.js';\r\n\r\ninterface PlanQuestionScreenProps {\r\n question: string;\r\n options: string[];\r\n onAnswer: (answer: string) => void;\r\n onSkip: () => void;\r\n}\r\n\r\nexport const PlanQuestionScreen: React.FC<PlanQuestionScreenProps> = ({\r\n question,\r\n options,\r\n onAnswer,\r\n onSkip\r\n}) => {\r\n const theme = useTheme();\r\n\r\n // All selectable items: options + \"Custom answer\" + \"Skip\"\r\n const allItems = [...options, '__custom__', '__skip__'];\r\n const [selectedIndex, setSelectedIndex] = useState(0);\r\n const [mode, setMode] = useState<'selecting' | 'custom-input'>('selecting');\r\n const [customValue, setCustomValue] = useState('');\r\n const submittedRef = useRef(false);\r\n\r\n useInput((input, key) => {\r\n if (submittedRef.current) return;\r\n\r\n if (mode === 'selecting') {\r\n if (key.upArrow) {\r\n setSelectedIndex(prev => (prev - 1 + allItems.length) % allItems.length);\r\n return;\r\n }\r\n if (key.downArrow) {\r\n setSelectedIndex(prev => (prev + 1) % allItems.length);\r\n return;\r\n }\r\n if (key.return) {\r\n const item = allItems[selectedIndex];\r\n if (item === '__skip__') {\r\n submittedRef.current = true;\r\n onSkip();\r\n } else if (item === '__custom__') {\r\n setMode('custom-input');\r\n } else {\r\n submittedRef.current = true;\r\n onAnswer(item);\r\n }\r\n return;\r\n }\r\n // Number shortcuts: 1-9 to quickly select options\r\n const num = parseInt(input, 10);\r\n if (!isNaN(num) && num >= 1 && num <= options.length) {\r\n submittedRef.current = true;\r\n onAnswer(options[num - 1]);\r\n return;\r\n }\r\n // 's' for skip\r\n if (input.toLowerCase() === 's') {\r\n submittedRef.current = true;\r\n onSkip();\r\n return;\r\n }\r\n }\r\n\r\n if (mode === 'custom-input') {\r\n // ESC to go back to selection mode\r\n if (key.escape) {\r\n setMode('selecting');\r\n setCustomValue('');\r\n return;\r\n }\r\n }\r\n });\r\n\r\n // Handle custom input submission (TextInput's onSubmit)\r\n const handleCustomSubmit = (value: string) => {\r\n if (submittedRef.current) return;\r\n const trimmed = value.trim();\r\n if (trimmed.length > 0) {\r\n submittedRef.current = true;\r\n onAnswer(trimmed);\r\n }\r\n };\r\n\r\n return (\r\n <Box\r\n flexDirection=\"column\"\r\n borderStyle=\"round\"\r\n borderColor=\"#f59e0b\"\r\n paddingX={1}\r\n paddingY={0}\r\n marginY={0}\r\n >\r\n {/* Question Header */}\r\n <Box marginBottom={0}>\r\n <Text color=\"#f59e0b\" bold>? </Text>\r\n <Text color=\"#ffffff\" bold wrap=\"wrap\">{question}</Text>\r\n </Box>\r\n\r\n {/* Options List (when in selecting mode) */}\r\n {mode === 'selecting' && (\r\n <Box flexDirection=\"column\" marginTop={0} marginLeft={1}>\r\n {options.map((option, index) => {\r\n const isSelected = selectedIndex === index;\r\n return (\r\n <Box key={index}>\r\n <Text color={isSelected ? theme.accent : '#666666'} bold={isSelected}>\r\n {isSelected ? '> ' : ' '}\r\n </Text>\r\n <Text color={isSelected ? '#ffffff' : '#999999'} bold={isSelected}>\r\n {index + 1}. {option}\r\n </Text>\r\n </Box>\r\n );\r\n })}\r\n\r\n {/* Custom Answer option */}\r\n <Box>\r\n <Text\r\n color={selectedIndex === options.length ? '#00bbff' : '#666666'}\r\n bold={selectedIndex === options.length}\r\n >\r\n {selectedIndex === options.length ? '> ' : ' '}\r\n </Text>\r\n <Text\r\n color={selectedIndex === options.length ? '#00bbff' : '#555555'}\r\n bold={selectedIndex === options.length}\r\n >\r\n Custom answer...\r\n </Text>\r\n </Box>\r\n\r\n {/* Skip option */}\r\n <Box>\r\n <Text\r\n color={selectedIndex === options.length + 1 ? '#888888' : '#666666'}\r\n bold={selectedIndex === options.length + 1}\r\n >\r\n {selectedIndex === options.length + 1 ? '> ' : ' '}\r\n </Text>\r\n <Text\r\n color={selectedIndex === options.length + 1 ? '#888888' : '#444444'}\r\n bold={selectedIndex === options.length + 1}\r\n italic\r\n >\r\n Skip (let AI decide)\r\n </Text>\r\n </Box>\r\n\r\n {/* Hint */}\r\n <Box marginTop={0}>\r\n <Text color=\"#555555\" dimColor>\r\n Arrow keys to navigate, Enter to select, 1-{options.length} for quick select, S to skip\r\n </Text>\r\n </Box>\r\n </Box>\r\n )}\r\n\r\n {/* Custom Input Mode */}\r\n {mode === 'custom-input' && (\r\n <Box flexDirection=\"column\" marginTop={0} marginLeft={1}>\r\n <Box>\r\n <Text color=\"#00bbff\" bold>Your answer: </Text>\r\n </Box>\r\n <Box\r\n borderStyle=\"round\"\r\n borderColor=\"#00bbff\"\r\n paddingX={1}\r\n paddingY={0}\r\n >\r\n <TextInput\r\n value={customValue}\r\n onChange={setCustomValue}\r\n onSubmit={handleCustomSubmit}\r\n placeholder=\"Type your answer and press Enter...\"\r\n />\r\n </Box>\r\n <Box marginTop={0}>\r\n <Text color=\"#555555\" dimColor>\r\n Enter to submit, ESC to go back to options\r\n </Text>\r\n </Box>\r\n </Box>\r\n )}\r\n </Box>\r\n );\r\n};\r\n\r\nexport default PlanQuestionScreen;\r\n"],"mappings":"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","names":[]}