本章总览
PromptInput.tsx(约 2300 行)实现 REPL 底部输入区:多行文本、Vim 模式、斜杠命令补全、权限模式切换、图片粘贴、语音插入、footer 状态与提交到 onSubmit。本章要求你能从按键事件追到 processUserInput,并理解与 overlay / hasSuppressedDialogs 的焦点互斥。
学完本章你应该能
- 列举 PromptInput Props 中与 REPL 对接的关键回调
- 说明 inputModes(!/#)与 permission mode 循环的关系
- 描述 insertTextRef 与语音/STT 的协作
- 理解 hasSuppressedDialogs 与 isLocalJSXCommandActive 的模态互斥
- 定位 PromptInputFooter、Notifications 的职责划分
核心概念(先读懂这些)
PromptInput 是「控制器」而非纯 TextInput
组件组合了 useInputBuffer、useTypeahead、useArrowKeyHistory、usePromptSuggestion、keybindings 等,自身维护 cursorOffset、helpOpen、exitMessage、isAutoUpdating。提交路径最终调用 REPL 传入的 onSubmit,并附带 PromptInputHelpers(含 getToolUseContext 等)。底部 50% maxHeight 限制来自 PROMPT_FOOTER_LINES 与 MIN_INPUT_VIEWPORT_LINES 常量。
模态叠加与导航键泄漏
local-jsx 命令(如 /mcp)可能 shouldHidePromptInput: false 但仍全屏遮罩。PromptInput 用 isModalOverlayActive || isLocalJSXCommandActive 阻止方向键穿透到 TextInput。hasSuppressedDialogs 在 REPL 侧表示权限/队列对话框占用焦点,footer 快捷键需让路。
建议学习步骤
- 阅读 Props 类型定义(源码块 A)
- 阅读 PromptInput 函数入口与模态检测(源码块 B)
- 查看 insertTextRef 与 trackAndSetInput(源码块 C)
- 打开 inputModes.ts 的 mode 解析(源码块 D)
- 对照 PromptInputFooter 与 Notifications(源码块 E、F)
常见误区
注意
外部修改 input(语音注入)会强制 cursor 移到末尾,勿与 Vim 光标逻辑冲突
注意
showBashesDialog 为 true 时会 early-return BackgroundTasksDialog,影响 companion 布局
注意
permission mode 切换写回 setToolPermissionContext,与 useCanUseTool 共用上下文
PromptInput 在布局中的位置
全屏 REPL 的 FullscreenLayout bottom slot 结构:
bottom
├─ permissionStickyFooter (可选)
├─ PromptInput
│ ├─ PromptInputModeIndicator
│ ├─ ShimmeredInput / TextInput / VimTextInput
│ ├─ PromptInputFooter (模型、权限、任务 pill)
│ └─ Notifications (临时状态行)
├─ immediate local-jsx commands
└─ CompanionSprite (宽屏右侧)
transcript 模式(screen === 'transcript')下 PromptInput 不挂载,editorStatus 改由 REPL footer 显示。QueuedCommands 在 scrollable 区底部(PromptInputQueuedCommands)展示排队斜杠命令。
Props:与 REPL 的契约
下列 Props 块定义 REPL ↔ PromptInput 接口(节选 124–189 行):
| Prop | 作用 |
|---|---|
| toolPermissionContext / setToolPermissionContext | 权限模式、auto mode |
| input / onInputChange / mode / onModeChange | 受控输入与 !/# 模式 |
| getToolUseContext | 构造 processUserInput 上下文 |
| onSubmit / onAgentSubmit | 主线程 vs agent 提交 |
| pastedContents / setPastedContents | 图片与长文本引用 |
| hasSuppressedDialogs | 权限等对话框占用 |
| insertTextRef | 外部插入(语音) |
| voiceInterimRange | 实时语音高亮区间 |
缺失 onAgentSubmit 时 agent 视图仍可能通过 teammate 管道注入消息,但底部输入行为不同。
源码引用: src/components/PromptInput/PromptInput.tsx · 第 124–189 行(共 3178 行)
124| import {
125| parseDirectMemberMessage,
126| sendDirectMemberMessage,
127| } from '../../utils/directMemberMessage.js'
128| import type { EffortLevel } from '../../utils/effort.js'
129| import { env } from '../../utils/env.js'
130| import { errorMessage } from '../../utils/errors.js'
131| import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'
132| import {
133| getFastModeUnavailableReason,
134| isFastModeAvailable,
135| isFastModeCooldown,
136| isFastModeEnabled,
137| isFastModeSupportedByModel,
138| } from '../../utils/fastMode.js'
139| import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
140| import type { PromptInputHelpers } from '../../utils/handlePromptSubmit.js'
141| import {
142| getImageFromClipboard,
143| PASTE_THRESHOLD,
144| } from '../../utils/imagePaste.js'
145| import type { ImageDimensions } from '../../utils/imageResizer.js'
146| import { cacheImagePath, storeImage } from '../../utils/imageStore.js'
147| import {
148| isMacosOptionChar,
149| MACOS_OPTION_SPECIAL_CHARS,
150| } from '../../utils/keyboardShortcuts.js'
151| import { logError } from '../../utils/log.js'
152| import {
153| isOpus1mMergeEnabled,
154| modelDisplayString,
155| } from '../../utils/model/model.js'
156| import { setAutoModeActive } from '../../utils/permissions/autoModeState.js'
157| import {
158| cyclePermissionMode,
159| getNextPermissionMode,
160| } from '../../utils/permissions/getNextPermissionMode.js'
161| import { transitionPermissionMode } from '../../utils/permissions/permissionSetup.js'
162| import { getPlatform } from '../../utils/platform.js'
163| import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
164| import { editPromptInEditor } from '../../utils/promptEditor.js'
165| import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
166| import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
167| import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
168| import {
169| findSlackChannelPositions,
170| getKnownChannelsVersion,
171| hasSlackMcpServer,
172| subscribeKnownChannels,
173| } from '../../utils/suggestions/slackChannelSuggestions.js'
174| import { isInProcessEnabled } from '../../utils/swarm/backends/registry.js'
175| import { syncTeammateMode } from '../../utils/swarm/teamHelpers.js'
176| import type { TeamSummary } from '../../utils/teamDiscovery.js'
177| import { getTeammateColor } from '../../utils/teammate.js'
178| import { isInProcessTeammate } from '../../utils/teammateContext.js'
179| import { writeToMailbox } from '../../utils/teammateMailbox.js'
180| import type { TextHighlight } from '../../utils/textHighlighting.js'
181| import type { Theme } from '../../utils/theme.js'
182| import {
183| findThinkingTriggerPositions,
184| getRainbowColor,
185| isUltrathinkEnabled,
186| } from '../../utils/thinking.js'
187| import { findTokenBudgetPositions } from '../../utils/tokenBudget.js'
188| import {
189| findUltraplanTriggerPositions,
源码引用: src/components/PromptInput/PromptInput.tsx · 第 191–193 行(共 3178 行)
191| } from '../../utils/ultraplan/keyword.js'
192| import { AutoModeOptInDialog } from '../AutoModeOptInDialog.js'
193| import { BridgeDialog } from '../BridgeDialog.js'
函数入口:模态与光标
PromptInput 函数体开头处理三类互斥:
- isModalOverlayActive || isLocalJSXCommandActive:视为 modal,阻断 footer 导航键
- input 外部变化:对比 lastInternalInputRef,非内部 set 则 cursor 跳到末尾(语音 STT)
- insertTextRef:暴露 insert / setInputWithCursor 给 REPL 层 voice 集成
trackAndSetInput 包装 onInputChange,保证内部编辑与外部注入可区分。
源码引用: src/components/PromptInput/PromptInput.tsx · 第 194–270 行(共 3178 行)
194| import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
195| import {
196| getVisibleAgentTasks,
197| useCoordinatorTaskCount,
198| } from '../CoordinatorAgentStatus.js'
199| import { getEffortNotificationText } from '../EffortIndicator.js'
200| import { getFastIconString } from '../FastIcon.js'
201| import { GlobalSearchDialog } from '../GlobalSearchDialog.js'
202| import { HistorySearchDialog } from '../HistorySearchDialog.js'
203| import { ModelPicker } from '../ModelPicker.js'
204| import { QuickOpenDialog } from '../QuickOpenDialog.js'
205| import TextInput from '../TextInput.js'
206| import { ThinkingToggle } from '../ThinkingToggle.js'
207| import { BackgroundTasksDialog } from '../tasks/BackgroundTasksDialog.js'
208| import { shouldHideTasksFooter } from '../tasks/taskStatusUtils.js'
209| import { TeamsDialog } from '../teams/TeamsDialog.js'
210| import VimTextInput from '../VimTextInput.js'
211| import { getModeFromInput, getValueFromInput } from './inputModes.js'
212| import {
213| FOOTER_TEMPORARY_STATUS_TIMEOUT,
214| Notifications,
215| } from './Notifications.js'
216| import PromptInputFooter from './PromptInputFooter.js'
217| import type { SuggestionItem } from './PromptInputFooterSuggestions.js'
218| import { PromptInputModeIndicator } from './PromptInputModeIndicator.js'
219| import { PromptInputQueuedCommands } from './PromptInputQueuedCommands.js'
220| import { PromptInputStashNotice } from './PromptInputStashNotice.js'
221| import { useMaybeTruncateInput } from './useMaybeTruncateInput.js'
222| import { usePromptInputPlaceholder } from './usePromptInputPlaceholder.js'
223| import { useShowFastIconHint } from './useShowFastIconHint.js'
224| import { useSwarmBanner } from './useSwarmBanner.js'
225| import { isNonSpacePrintable, isVimModeEnabled } from './utils.js'
226|
227| type Props = {
228| debug: boolean
229| ideSelection: IDESelection | undefined
230| toolPermissionContext: ToolPermissionContext
231| setToolPermissionContext: (ctx: ToolPermissionContext) => void
232| apiKeyStatus: VerificationStatus
233| commands: Command[]
234| agents: AgentDefinition[]
235| isLoading: boolean
236| verbose: boolean
237| messages: Message[]
238| onAutoUpdaterResult: (result: AutoUpdaterResult) => void
239| autoUpdaterResult: AutoUpdaterResult | null
240| input: string
241| onInputChange: (value: string) => void
242| mode: PromptInputMode
243| onModeChange: (mode: PromptInputMode) => void
244| stashedPrompt:
245| | {
246| text: string
247| cursorOffset: number
248| pastedContents: Record<number, PastedContent>
249| }
250| | undefined
251| setStashedPrompt: (
252| value:
253| | {
254| text: string
255| cursorOffset: number
256| pastedContents: Record<number, PastedContent>
257| }
258| | undefined,
259| ) => void
260| submitCount: number
261| onShowMessageSelector: () => void
262| /** Fullscreen message actions: shift+↑ enters cursor. */
263| onMessageActionsEnter?: () => void
264| mcpClients: MCPServerConnection[]
265| pastedContents: Record<number, PastedContent>
266| setPastedContents: React.Dispatch<
267| React.SetStateAction<Record<number, PastedContent>>
268| >
269| vimMode: VimMode
270| setVimMode: (mode: VimMode) => void
inputModes 与提交前缀
inputModes.ts 提供:
getModeFromInput/getValueFromInput:解析 leading!bash 模式、#记忆模式等prependModeCharacterToInput:模式切换时保留字符
REPL 在 history 与 submit 路径调用这些函数,PromptInputModeIndicator 向用户展示当前模式。permission mode(plan/auto/bypass)通过 cyclePermissionMode 与 footer 点击切换,写入 toolPermissionContext,影响下一轮 canUseTool 求值。
源码引用: src/components/PromptInput/inputModes.ts · 第 1–34 行(共 34 行)
1| import type { HistoryMode } from 'src/hooks/useArrowKeyHistory.js'
2| import type { PromptInputMode } from 'src/types/textInputTypes.js'
3|
4| export function prependModeCharacterToInput(
5| input: string,
6| mode: PromptInputMode,
7| ): string {
8| switch (mode) {
9| case 'bash':
10| return `!${input}`
11| default:
12| return input
13| }
14| }
15|
16| export function getModeFromInput(input: string): HistoryMode {
17| if (input.startsWith('!')) {
18| return 'bash'
19| }
20| return 'prompt'
21| }
22|
23| export function getValueFromInput(input: string): string {
24| const mode = getModeFromInput(input)
25| if (mode === 'prompt') {
26| return input
27| }
28| return input.slice(1)
29| }
30|
31| export function isInputModeCharacter(input: string): boolean {
32| return input === '!'
33| }
34|
源码引用: src/components/PromptInput/PromptInputModeIndicator.tsx · 第 1–40 行(共 105 行)
1| import figures from 'figures'
2| import * as React from 'react'
3| import { Box, Text } from 'src/ink.js'
4| import {
5| AGENT_COLOR_TO_THEME_COLOR,
6| AGENT_COLORS,
7| type AgentColorName,
8| } from 'src/tools/AgentTool/agentColorManager.js'
9| import type { PromptInputMode } from 'src/types/textInputTypes.js'
10| import { getTeammateColor } from 'src/utils/teammate.js'
11| import type { Theme } from 'src/utils/theme.js'
12| import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
13|
14| type Props = {
15| mode: PromptInputMode
16| isLoading: boolean
17| viewingAgentName?: string
18| viewingAgentColor?: AgentColorName
19| }
20|
21| /**
22| * Gets the theme color key for the teammate's assigned color.
23| * Returns undefined if not a teammate or if the color is invalid.
24| */
25| function getTeammateThemeColor(): keyof Theme | undefined {
26| if (!isAgentSwarmsEnabled()) {
27| return undefined
28| }
29| const colorName = getTeammateColor()
30| if (!colorName) {
31| return undefined
32| }
33| if (AGENT_COLORS.includes(colorName as AgentColorName)) {
34| return AGENT_COLOR_TO_THEME_COLOR[colorName as AgentColorName]
35| }
36| return undefined
37| }
38|
39| type PromptCharProps = {
40| isLoading: boolean
输入控件:TextInput 与 Vim
PromptInput 在 isVimModeEnabled() 时渲染 VimTextInput,否则 ShimmeredInput 或 TextInput。
相关 hooks:
- useInputBuffer:多行编辑、软换行
- useMaybeTruncateInput:极长输入截断显示
- usePromptInputPlaceholder:占位符随 loading/agent 变化
- useTypeahead / usePromptSuggestion:斜杠命令与 prompt 建议
- useArrowKeyHistory / useHistorySearch:HistorySearchDialog
图片粘贴走 imagePaste / imageStore;超过 PASTE_THRESHOLD 转 pastedContents 引用号。
源码引用: src/components/PromptInput/ShimmeredInput.tsx · 第 1–35 行(共 122 行)
1| import * as React from 'react'
2| import { Ansi, Box, Text, useAnimationFrame } from '../../ink.js'
3| import {
4| segmentTextByHighlights,
5| type TextHighlight,
6| } from '../../utils/textHighlighting.js'
7| import { ShimmerChar } from '../Spinner/ShimmerChar.js'
8|
9| type Props = {
10| text: string
11| highlights: TextHighlight[]
12| }
13|
14| type LinePart = {
15| text: string
16| highlight: TextHighlight | undefined
17| start: number
18| }
19|
20| export function HighlightedInput({ text, highlights }: Props): React.ReactNode {
21| // The shimmer animation (below) re-renders this component at 20fps while the
22| // ultrathink keyword is present. text/highlights are referentially stable
23| // across animation ticks (parent doesn't re-render), so memoize everything
24| // that derives from them: segmentTextByHighlights alone is ~85µs/call
25| // (tokenize + sort + O(n²) overlap), which adds up fast at 20fps.
26| const { lines, hasShimmer, sweepStart, cycleLength } = React.useMemo(() => {
27| const segments = segmentTextByHighlights(text, highlights)
28|
29| // Split segments by newlines into per-line groups. Ink's row-direction Box
30| // indents continuation lines of a multi-line child to that child's X offset.
31| // By splitting at newlines, each line renders as its own row, avoiding the
32| // incorrect indentation when highlighted text is followed by wrapped content.
33| const lines: LinePart[][] = [[]]
34| let pos = 0
35| for (const segment of segments) {
源码引用: src/components/PromptInput/useMaybeTruncateInput.ts · 第 1–30 行(共 59 行)
1| import { useEffect, useState } from 'react'
2| import type { PastedContent } from 'src/utils/config.js'
3| import { maybeTruncateInput } from './inputPaste.js'
4|
5| type Props = {
6| input: string
7| pastedContents: Record<number, PastedContent>
8| onInputChange: (input: string) => void
9| setCursorOffset: (offset: number) => void
10| setPastedContents: (contents: Record<number, PastedContent>) => void
11| }
12|
13| export function useMaybeTruncateInput({
14| input,
15| pastedContents,
16| onInputChange,
17| setCursorOffset,
18| setPastedContents,
19| }: Props) {
20| // Track if we've initialized this specific input value
21| const [hasAppliedTruncationToInput, setHasAppliedTruncationToInput] =
22| useState(false)
23|
24| // Process input for truncation and pasted images from MessageSelector.
25| useEffect(() => {
26| if (hasAppliedTruncationToInput) {
27| return
28| }
29|
30| if (input.length <= 10_000) {
PromptInputFooter 与状态 pill
Footer 左侧(PromptInputFooterLeftSide)聚合:
- 模型名、fast mode、effort、opus merge 提示
- 权限模式图标与切换
- Coordinator agent 数量、background tasks
- IDE @ mention、sandbox hint
Footer 右侧 suggestions(PromptInputFooterSuggestions)展示可点击 suggestion item。
shouldHideTasksFooter 等 util 避免 agent 运行时重复展示 tasks 区域。Footer 与 Notifications 分工:footer 偏持久配置,Notifications 偏临时事件(FOOTER_TEMPORARY_STATUS_TIMEOUT)。
源码引用: src/components/PromptInput/PromptInputFooter.tsx · 第 1–45 行(共 280 行)
1| import { feature } from 'bun:bundle'
2| import * as React from 'react'
3| import { memo, type ReactNode, useMemo, useRef } from 'react'
4| import { isBridgeEnabled } from '../../bridge/bridgeEnabled.js'
5| import { getBridgeStatus } from '../../bridge/bridgeStatusUtil.js'
6| import { useSetPromptOverlay } from '../../context/promptOverlayContext.js'
7| import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
8| import type { IDESelection } from '../../hooks/useIdeSelection.js'
9| import { useSettings } from '../../hooks/useSettings.js'
10| import { useTerminalSize } from '../../hooks/useTerminalSize.js'
11| import { Box, Text } from '../../ink.js'
12| import type { MCPServerConnection } from '../../services/mcp/types.js'
13| import { useAppState } from '../../state/AppState.js'
14| import type { ToolPermissionContext } from '../../Tool.js'
15| import type { Message } from '../../types/message.js'
16| import type { PromptInputMode, VimMode } from '../../types/textInputTypes.js'
17| import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
18| import { isFullscreenEnvEnabled } from '../../utils/fullscreen.js'
19| import { isUndercover } from '../../utils/undercover.js'
20| import {
21| CoordinatorTaskPanel,
22| useCoordinatorTaskCount,
23| } from '../CoordinatorAgentStatus.js'
24| import {
25| getLastAssistantMessageId,
26| StatusLine,
27| statusLineShouldDisplay,
28| } from '../StatusLine.js'
29| import { Notifications } from './Notifications.js'
30| import { PromptInputFooterLeftSide } from './PromptInputFooterLeftSide.js'
31| import {
32| PromptInputFooterSuggestions,
33| type SuggestionItem,
34| } from './PromptInputFooterSuggestions.js'
35| import { PromptInputHelpMenu } from './PromptInputHelpMenu.js'
36|
37| type Props = {
38| apiKeyStatus: VerificationStatus
39| debug: boolean
40| exitMessage: {
41| show: boolean
42| key?: string
43| }
44| vimMode: VimMode | undefined
45| mode: PromptInputMode
源码引用: src/components/PromptInput/Notifications.tsx · 第 1–40 行(共 367 行)
1| import { feature } from 'bun:bundle'
2| import * as React from 'react'
3| import { type ReactNode, useEffect, useMemo, useState } from 'react'
4| import {
5| type Notification,
6| useNotifications,
7| } from 'src/context/notifications.js'
8| import { logEvent } from 'src/services/analytics/index.js'
9| import { useAppState } from 'src/state/AppState.js'
10| import { useVoiceState } from '../../context/voice.js'
11| import type { VerificationStatus } from '../../hooks/useApiKeyVerification.js'
12| import { useIdeConnectionStatus } from '../../hooks/useIdeConnectionStatus.js'
13| import type { IDESelection } from '../../hooks/useIdeSelection.js'
14| import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
15| import { useVoiceEnabled } from '../../hooks/useVoiceEnabled.js'
16| import { Box, Text } from '../../ink.js'
17| import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
18| import { calculateTokenWarningState } from '../../services/compact/autoCompact.js'
19| import type { MCPServerConnection } from '../../services/mcp/types.js'
20| import type { Message } from '../../types/message.js'
21| import {
22| getApiKeyHelperElapsedMs,
23| getConfiguredApiKeyHelper,
24| getSubscriptionType,
25| } from '../../utils/auth.js'
26| import type { AutoUpdaterResult } from '../../utils/autoUpdater.js'
27| import { getExternalEditor } from '../../utils/editor.js'
28| import { isEnvTruthy } from '../../utils/envUtils.js'
29| import { formatDuration } from '../../utils/format.js'
30| import { setEnvHookNotifier } from '../../utils/hooks/fileChangedWatcher.js'
31| import { toIDEDisplayName } from '../../utils/ide.js'
32| import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
33| import { tokenCountFromLastAPIResponse } from '../../utils/tokens.js'
34| import { AutoUpdaterWrapper } from '../AutoUpdaterWrapper.js'
35| import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
36| import { IdeStatusIndicator } from '../IdeStatusIndicator.js'
37| import { MemoryUsageIndicator } from '../MemoryUsageIndicator.js'
38| import { SentryErrorBoundary } from '../SentryErrorBoundary.js'
39| import { TokenWarning } from '../TokenWarning.js'
40| import { SandboxPromptFooterHint } from './SandboxPromptFooterHint.js'
提交路径与 speculation
onSubmit prop 类型包含可选 speculationAccept:
- 与 PromptSuggestion speculation 服务联动
- 接受预生成内容时记录 session 节省时间
REPL 传入的 onSubmit 最终进入 handlePromptSubmit / processUserInput,携带:
- helpers:abort、addToHistory、expandPastedTextRefs
- options.fromKeybinding:区分键绑提交与按钮提交
onAgentSubmit 在查看 teammate/local agent 时走并行管道,不经过主线程 query。
源码引用: src/components/PromptInput/PromptInput.tsx · 第 164–171 行(共 3178 行)
164| import { editPromptInEditor } from '../../utils/promptEditor.js'
165| import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
166| import { findBtwTriggerPositions } from '../../utils/sideQuestion.js'
167| import { findSlashCommandPositions } from '../../utils/suggestions/commandSuggestions.js'
168| import {
169| findSlackChannelPositions,
170| getKnownChannelsVersion,
171| hasSlackMcpServer,
源码引用: src/utils/handlePromptSubmit.ts · 第 1–40 行(共 611 行)
1| import type { UUID } from 'crypto'
2| import { logEvent } from 'src/services/analytics/index.js'
3| import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from 'src/services/analytics/metadata.js'
4| import { type Command, getCommandName, isCommandEnabled } from '../commands.js'
5| import { selectableUserMessagesFilter } from '../components/MessageSelector.js'
6| import type { SpinnerMode } from '../components/Spinner/types.js'
7| import type { QuerySource } from '../constants/querySource.js'
8| import { expandPastedTextRefs, parseReferences } from '../history.js'
9| import type { CanUseToolFn } from '../hooks/useCanUseTool.js'
10| import type { IDESelection } from '../hooks/useIdeSelection.js'
11| import type { AppState } from '../state/AppState.js'
12| import type { SetToolJSXFn } from '../Tool.js'
13| import type { LocalJSXCommandOnDone } from '../types/command.js'
14| import type { Message } from '../types/message.js'
15| import {
16| isValidImagePaste,
17| type PromptInputMode,
18| type QueuedCommand,
19| } from '../types/textInputTypes.js'
20| import { createAbortController } from './abortController.js'
21| import type { PastedContent } from './config.js'
22| import { logForDebugging } from './debug.js'
23| import type { EffortValue } from './effort.js'
24| import type { FileHistoryState } from './fileHistory.js'
25| import { fileHistoryEnabled, fileHistoryMakeSnapshot } from './fileHistory.js'
26| import { gracefulShutdownSync } from './gracefulShutdown.js'
27| import { enqueue } from './messageQueueManager.js'
28| import { resolveSkillModelOverride } from './model/model.js'
29| import type { ProcessUserInputContext } from './processUserInput/processUserInput.js'
30| import { processUserInput } from './processUserInput/processUserInput.js'
31| import type { QueryGuard } from './QueryGuard.js'
32| import { queryCheckpoint, startQueryProfile } from './queryProfiler.js'
33| import { runWithWorkload } from './workloadContext.js'
34|
35| function exit(): void {
36| gracefulShutdownSync(0)
37| }
38|
39| type BaseExecutionParams = {
40| queuedCommands?: QueuedCommand[]
辅助组件与 banner
| 文件 | 职责 |
|---|---|
| PromptInputHelpMenu | 快捷键与帮助浮层 |
| PromptInputQueuedCommands | 展示排队命令(scrollable 区也有) |
| PromptInputStashNotice | stashed prompt 恢复提示 |
| IssueFlagBanner | issue 标记横幅 |
| SandboxPromptFooterHint | sandbox 模式说明 |
| VoiceIndicator | 语音模式指示 |
| useSwarmBanner | swarm 团队横幅 |
HistorySearchInput 在 isSearchingHistory 时替换或叠加输入区。GlobalSearchDialog、ModelPicker 等由 footer 快捷键打开,通过 overlayContext 协调。
源码引用: src/components/PromptInput/PromptInputHelpMenu.tsx · 第 1–30 行(共 150 行)
1| import { feature } from 'bun:bundle'
2| import * as React from 'react'
3| import { Box, Text } from 'src/ink.js'
4| import { getPlatform } from 'src/utils/platform.js'
5| import { isKeybindingCustomizationEnabled } from '../../keybindings/loadUserBindings.js'
6| import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
7| import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
8| import { isFastModeAvailable, isFastModeEnabled } from '../../utils/fastMode.js'
9| import { getNewlineInstructions } from './utils.js'
10|
11| /** Format a shortcut for display in the help menu (e.g., "ctrl+o" → "ctrl + o") */
12| function formatShortcut(shortcut: string): string {
13| return shortcut.replace(/\+/g, ' + ')
14| }
15|
16| type Props = {
17| dimColor?: boolean
18| fixedWidth?: boolean
19| gap?: number
20| paddingX?: number
21| }
22|
23| export function PromptInputHelpMenu(props: Props): React.ReactNode {
24| const { dimColor, fixedWidth, gap, paddingX } = props
25|
26| // Get configured shortcuts from keybinding system
27| const transcriptShortcut = formatShortcut(
28| useShortcutDisplay('app:toggleTranscript', 'Global', 'ctrl+o'),
29| )
30| const todosShortcut = formatShortcut(
源码引用: src/components/PromptInput/IssueFlagBanner.tsx · 第 1–25 行(共 29 行)
1| import * as React from 'react'
2| import { FLAG_ICON } from '../../constants/figures.js'
3| import { Box, Text } from '../../ink.js'
4|
5| /**
6| * ANT-ONLY: Banner shown in the transcript that prompts users to report
7| * issues via /issue. Appears when friction is detected in the conversation.
8| */
9| export function IssueFlagBanner(): React.ReactNode {
10| if ("external" !== 'ant') {
11| return null
12| }
13|
14| return (
15| <Box flexDirection="row" marginTop={1} width="100%">
16| <Box minWidth={2}>
17| <Text color="warning">{FLAG_ICON}</Text>
18| </Box>
19| <Text>
20| <Text dimColor>[ANT-ONLY] </Text>
21| <Text color="warning" bold>
22| Something off with Claude?
23| </Text>
24| <Text dimColor> /issue to report it</Text>
25| </Text>
REPL 侧对接要点
REPL 渲染 PromptInput 时传入:
- messages:用于 context suggestions、@file 解析
- isLoading:禁用提交、改 placeholder
- hasSuppressedDialogs:toolUseConfirmQueue 等有项时为 true
- disabled prop(REPL 级):整个输入隐藏
当 focusedInputDialog 非空,companion 与部分 footer 交互隐藏。Permission 批准期间用户焦点在 overlay,PromptInput 仍可见但键盘事件由 Permission 组件消费。
阅读 PromptInput 时宜对照 REPL 4590+ 行 bottom JSX 传参列表。
源码引用: src/screens/REPL.tsx · 第 58–59 行(共 7050 行)
58| mergeFileStateCaches,
59| READ_FILE_STATE_CACHE_SIZE,
源码引用: src/components/PromptInput/PromptInputQueuedCommands.tsx · 第 1–30 行(共 167 行)
1| import { feature } from 'bun:bundle'
2| import * as React from 'react'
3| import { useMemo } from 'react'
4| import { Box } from 'src/ink.js'
5| import { useAppState } from 'src/state/AppState.js'
6| import {
7| STATUS_TAG,
8| SUMMARY_TAG,
9| TASK_NOTIFICATION_TAG,
10| } from '../../constants/xml.js'
11| import { QueuedMessageProvider } from '../../context/QueuedMessageContext.js'
12| import { useCommandQueue } from '../../hooks/useCommandQueue.js'
13| import type { QueuedCommand } from '../../types/textInputTypes.js'
14| import { isQueuedCommandVisible } from '../../utils/messageQueueManager.js'
15| import {
16| createUserMessage,
17| EMPTY_LOOKUPS,
18| normalizeMessages,
19| } from '../../utils/messages.js'
20| import { jsonParse } from '../../utils/slowOperations.js'
21| import { Message } from '../Message.js'
22|
23| const EMPTY_SET = new Set<string>()
24|
25| /**
26| * Check if a command value is an idle notification that should be hidden.
27| * Idle notifications are processed silently without showing to the user.
28| */
29| function isIdleNotification(value: string): boolean {
30| try {
源码目录
PromptInput/ 子目录 21 文件,建议从 PromptInput.tsx → Footer → inputModes 顺序阅读。
动手练习
- 切换 permission mode(快捷键或 footer),观察 setToolPermissionContext 后下一次 tool_use 是否仍 ask
- 开启 Vim 模式,确认 hjkl 不触发 transcript 滚动
- 粘贴超大文本,检查 pastedContents 引用是否出现在提交后的 UserPromptMessage
- 在权限弹窗打开时尝试输入,验证 hasSuppressedDialogs 行为
本章小结与延伸
PromptInput = 用户意图入口。提交后见 query 引擎;等待批准时 REPL 抑制输入但组件可能仍挂载。 继续学习: