CommandInput.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. /**
  2. * CommandInput component - input field with autocomplete, approval, and followup suggestions support
  3. * Updated to use useCommandInput, useWebviewMessage, useApprovalHandler, and useFollowupSuggestions hooks
  4. */
  5. import React, { useCallback, useEffect } from "react"
  6. import { Box, Text } from "ink"
  7. import { useSetAtom, useAtomValue, useAtom } from "jotai"
  8. import { submissionCallbackAtom } from "../../state/atoms/keyboard.js"
  9. import {
  10. selectedIndexAtom,
  11. inputModeAtom,
  12. isCommittingParallelModeAtom,
  13. commitCountdownSecondsAtom,
  14. } from "../../state/atoms/ui.js"
  15. import { shellModeActiveAtom, executeShellCommandAtom } from "../../state/atoms/keyboard.js"
  16. import { MultilineTextInput } from "./MultilineTextInput.js"
  17. import { useCommandInput } from "../../state/hooks/useCommandInput.js"
  18. import { useApprovalHandler } from "../../state/hooks/useApprovalHandler.js"
  19. import { useFollowupSuggestions } from "../../state/hooks/useFollowupSuggestions.js"
  20. import { useTheme } from "../../state/hooks/useTheme.js"
  21. import { AutocompleteMenu } from "./AutocompleteMenu.js"
  22. import { ApprovalMenu } from "./ApprovalMenu.js"
  23. import { FollowupSuggestionsMenu } from "./FollowupSuggestionsMenu.js"
  24. import { useResetAtom } from "jotai/utils"
  25. interface CommandInputProps {
  26. onSubmit: (value: string) => void
  27. placeholder?: string
  28. disabled?: boolean
  29. }
  30. export const CommandInput: React.FC<CommandInputProps> = ({
  31. onSubmit,
  32. placeholder = "Type a message or /command...",
  33. disabled = false,
  34. }) => {
  35. // Get theme colors
  36. const theme = useTheme()
  37. // Get shell mode state
  38. const isShellModeActive = useAtomValue(shellModeActiveAtom)
  39. const inputMode = useAtomValue(inputModeAtom)
  40. const executeShellCommand = useSetAtom(executeShellCommandAtom)
  41. // Use the command input hook for autocomplete functionality
  42. const { isAutocompleteVisible, commandSuggestions, argumentSuggestions, fileMentionSuggestions } = useCommandInput()
  43. // Use the approval handler hook for approval functionality
  44. // This hook sets up the approval callbacks that the keyboard handler uses
  45. const { isApprovalPending, approvalOptions } = useApprovalHandler()
  46. // Use the followup suggestions hook
  47. const { suggestions: followupSuggestions, isVisible: isFollowupVisible } = useFollowupSuggestions()
  48. // Setup centralized keyboard handler
  49. const setSubmissionCallback = useSetAtom(submissionCallbackAtom)
  50. const sharedSelectedIndex = useAtomValue(selectedIndexAtom)
  51. const isCommittingParallelMode = useAtomValue(isCommittingParallelModeAtom)
  52. const [countdownSeconds, setCountdownSeconds] = useAtom(commitCountdownSecondsAtom)
  53. const resetCountdownSeconds = useResetAtom(commitCountdownSecondsAtom)
  54. // Countdown timer effect for parallel mode commit
  55. useEffect(() => {
  56. if (!isCommittingParallelMode) {
  57. resetCountdownSeconds()
  58. return
  59. }
  60. resetCountdownSeconds()
  61. const interval = setInterval(() => {
  62. setCountdownSeconds((prev) => {
  63. if (prev <= 1) {
  64. clearInterval(interval)
  65. return 0
  66. }
  67. return prev - 1
  68. })
  69. }, 1000)
  70. return () => clearInterval(interval)
  71. }, [isCommittingParallelMode, setCountdownSeconds, resetCountdownSeconds])
  72. // Determine suggestion type for autocomplete menu
  73. const suggestionType =
  74. fileMentionSuggestions.length > 0
  75. ? "file-mention"
  76. : commandSuggestions.length > 0
  77. ? "command"
  78. : argumentSuggestions.length > 0
  79. ? "argument"
  80. : "none"
  81. // Determine if input should be disabled (during approval, when explicitly disabled, or when committing parallel mode)
  82. const isInputDisabled = disabled || isApprovalPending || isCommittingParallelMode
  83. // Enhanced submission handler for shell mode
  84. const handleSubmit = useCallback(
  85. (value: string) => {
  86. if (isShellModeActive) {
  87. // Execute as shell command
  88. executeShellCommand(value)
  89. } else {
  90. // Normal submission
  91. onSubmit(value)
  92. }
  93. },
  94. [isShellModeActive, executeShellCommand, onSubmit],
  95. )
  96. // Set the submission callback so keyboard handler can trigger onSubmit
  97. useEffect(() => {
  98. setSubmissionCallback({ callback: handleSubmit })
  99. }, [handleSubmit, setSubmissionCallback])
  100. // Determine styling based on mode (priority: parallel mode > shell mode > approval > normal)
  101. const isShellMode = inputMode === "shell"
  102. const borderColor = isCommittingParallelMode
  103. ? theme.ui.border.active
  104. : isShellMode
  105. ? theme.semantic.warning
  106. : isApprovalPending
  107. ? theme.actions.pending
  108. : theme.ui.border.active
  109. const promptColor = isCommittingParallelMode
  110. ? theme.ui.border.active
  111. : isShellMode
  112. ? theme.semantic.warning
  113. : isApprovalPending
  114. ? theme.actions.pending
  115. : theme.ui.border.active
  116. const promptSymbol = isCommittingParallelMode ? "⏳ " : isShellMode ? "$ " : isApprovalPending ? "[!] " : "> "
  117. const inputPlaceholder = isCommittingParallelMode
  118. ? `Committing your changes... (${countdownSeconds}s)`
  119. : isShellMode
  120. ? "Type shell command..."
  121. : isApprovalPending
  122. ? "Awaiting approval..."
  123. : placeholder
  124. return (
  125. <Box flexDirection="column">
  126. {/* Input field */}
  127. <Box borderStyle="round" borderColor={borderColor} paddingX={1}>
  128. <Box flexDirection="row" alignItems="center">
  129. <Text color={promptColor} bold>
  130. {isShellMode && !isCommittingParallelMode && (
  131. <>
  132. <Text color={promptColor}>shell</Text>
  133. <Text> </Text>
  134. </>
  135. )}
  136. {promptSymbol}
  137. </Text>
  138. <MultilineTextInput
  139. placeholder={inputPlaceholder}
  140. showCursor={!isInputDisabled}
  141. maxLines={5}
  142. width={Math.max(10, isShellMode ? process.stdout.columns - 12 : process.stdout.columns - 6)}
  143. focus={!isInputDisabled}
  144. />
  145. </Box>
  146. </Box>
  147. {/* Approval menu - shown above input when approval is pending */}
  148. <ApprovalMenu options={approvalOptions} selectedIndex={sharedSelectedIndex} visible={isApprovalPending} />
  149. {/* Followup suggestions menu - shown when followup question is active (takes priority over autocomplete) */}
  150. {!isApprovalPending && isFollowupVisible && (
  151. <FollowupSuggestionsMenu
  152. suggestions={followupSuggestions}
  153. selectedIndex={sharedSelectedIndex}
  154. visible={isFollowupVisible}
  155. />
  156. )}
  157. {/* Autocomplete menu - only shown when not in approval mode and no followup suggestions */}
  158. {!isApprovalPending && !isFollowupVisible && (
  159. <AutocompleteMenu
  160. type={suggestionType}
  161. commandSuggestions={commandSuggestions}
  162. argumentSuggestions={argumentSuggestions}
  163. fileMentionSuggestions={fileMentionSuggestions}
  164. selectedIndex={sharedSelectedIndex}
  165. visible={isAutocompleteVisible}
  166. />
  167. )}
  168. </Box>
  169. )
  170. }