TUI 核心组件
本文档详细介绍 OpenCode TUI 的核心组件及其功能。
Prompt 组件
功能概述
Prompt 组件是用户与 AI 交互的核心输入组件,提供丰富的输入功能。
主要特性
| 特性 | 说明 |
|---|---|
| 多行输入 | 支持 1-6 行高度自适应 |
| 语法高亮 | Markdown 语法实时高亮 |
| 虚拟文本 | 文件/图像引用显示为占位符 |
| 自动补全 | 智能补全文件、命令、agent |
| 历史记录 | 上下键导航历史 |
| 暂存功能 | 多个提示的快速切换 |
| Shell 模式 | ! 前缀执行 shell 命令 |
组件结构
虚拟文本(extmarks)
虚拟文本允许在输入框中显示文件引用的占位符,节省空间:
// 创建文件引用的虚拟文本
const extmarkId = input.extmarks.create({
start: extmarkStart,
end: extmarkEnd,
virtual: true,
styleId: fileStyleId,
typeId: promptPartTypeId,
})
效果: [Image 1] 点击展开为实际内容
自动补全
提供以下补全类型:
| 类型 | 触发条件 | 内容 |
|---|---|---|
| 文件 | / 开头 | 文件路径 |
| 命令 | / 开头 | 斜杠命令 |
| Agent | @ 开头 | Agent 名称 |
| 链接 | http 开头 | URL 验证 |
历史记录
// 上下键导航
if (keybind.match("history_previous", e)) {
const item = history.move(-1, input.plainText)
if (item) {
input.setText(item.input)
// ...
}
}
Shell 模式
以 ! 开头进入 Shell 模式:
if (e.name === "!" && input.visualCursor.offset === 0) {
setStore("mode", "shell")
e.preventDefault()
return
}
// 提交时
if (store.mode === "shell") {
sdk.client.session.shell({
sessionID,
agent: local.agent.current().name,
command: inputText,
})
}
API
export type PromptRef = {
focused: boolean
current: PromptInfo
set(prompt: PromptInfo): void
reset(): void
blur(): void
focus(): void
submit(): void
}
export type PromptProps = {
sessionID?: string
visible?: boolean
disabled?: boolean
onSubmit?: () => void
ref?: (ref: PromptRef) => void
hint?: JSX.Element
showPlaceholder?: boolean
}
消息组件
UserMessage
显示用户发送的消息,包含文本和文件附件。
function UserMessage(props: {
message: UserMessage
parts: Part[]
onMouseUp: () => void
index: number
pending?: string
})
结构:
┌────────────────────────────┐
│ 用户消息内容 │
│ 📄 filename.txt │
│ 📷 Image 1 │
│ [时间戳/QUEUED 标记] │
└────────────────────────────┘
特性:
- 左边框颜色标识 Agent
- 鼠标悬停显示背景高亮
- 点击打开消息菜单
- 支持显示图像、PDF、文件附件
AssistantMessage
显示 AI 助手的响应消息。
function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean })
消息部分映射:
const PART_MAPPING = {
text: TextPart, // 普通文本
tool: ToolPart, // 工具调用
reasoning: ReasoningPart, // 推理过程
}
渲染逻辑:
<For each={props.parts}>
{(part, index) => {
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
return (
<Show when={component()}>
<Dynamic
last={index() === props.parts.length - 1}
component={component()}
part={part}
message={props.message}
/>
</Show>
)
}}
</For>
TextPart
渲染 Markdown 文本内容。
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true} // 流式渲染
syntaxStyle={syntax()} // 语法高亮
content={props.part.text.trim()}
conceal={ctx.conceal()} // 代码折叠
fg={theme.text}
/>
ReasoningPart
显示 AI 的推理过程(如 Claude 的 thinking)。
<Show when={content() && ctx.showThinking()}>
<box border={["left"]} borderColor={theme.backgroundElement}>
<code
filetype="markdown"
streaming={true}
syntaxStyle={subtleSyntax()}
content={"_Thinking:_ " + content()}
conceal={ctx.conceal()}
fg={theme.textMuted}
/>
</box>
</Show>
特性:
- 可折叠显示(
showThinking状态) - 过滤掉 OpenRouter 的 REDACTED 内容
- 使用灰色文本标识
ToolPart
根据工具类型动态渲染工具输出。
<Switch>
<Match when={props.part.tool === "bash"}>
<Bash {...toolprops} />
</Match>
<Match when={props.part.tool === "glob"}>
<Glob {...toolprops} />
</Match>
<Match when={props.part.tool === "read"}>
<Read {...toolprops} />
</Match>
<Match when={props.part.tool === "write"}>
<Write {...toolprops} />
</Match>
<Match when={props.part.tool === "edit"}>
<Edit {...toolprops} />
</Match>
<Match when={props.part.tool === "grep"}>
<Grep {...toolprops} />
</Match>
<Match when={props.part.tool === "task"}>
<Task {...toolprops} />
</Match>
<Match when={props.part.tool === "todowrite"}>
<TodoWrite {...toolprops} />
</Match>
<Match when={props.part.tool === "question"}>
<Question {...toolprops} />
</Match>
<Match when={true}>
<GenericTool {...toolprops} />
</Match>
</Switch>
工具组件类型:
| 工具 | 组件 | 显示方式 |
|---|---|---|
| bash | Bash | 内联或块级 |
| read | Read | 代码块 |
| write | Write | 内联 |
| edit | Edit | 差异显示 |
| glob | Glob | 内联 |
| grep | Grep | 内联或块级 |
| task | Task | 内联 |
| todowrite | TodoWrite | 内联 |
| question | Question | 内联 |
Sidebar 组件
显示会话的上下文信息和状态。
分区说明
1. Context 区
显示上下文使用情况:
const context = createMemo(() => {
const last = messages().findLast((x) => x.role === "assistant")
const total =
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
const model = sync.data.provider.find((x) => x.id === last.providerID)?.models[last.modelID]
return {
tokens: total.toLocaleString(),
percentage: model?.limit.context ? Math.round((total / model.limit.context) * 100) : null,
}
})
显示内容:
- Token 总数
- 上下文使用百分比
- 当前会话成本
2. MCP 区
显示 MCP 服务器状态:
<For each={mcpEntries()}>
{([key, item]) => (
<text fg={statusColor[item.status]}>•</text>
<text>{key}</text>
<text fg={theme.textMuted}>{statusText[item.status]}</text>
)}
</For>
状态映射:
| 状态 | 颜色 | 文本 |
|---|---|---|
| connected | success | Connected |
| failed | error | 错误信息 |
| disabled | textMuted | Disabled |
| needs_auth | warning | Needs auth |
| needs_client_registration | error | Needs client ID |
折叠逻辑: 服务器数量 > 2 时可折叠
3. LSP 区
显示语言服务器状态:
<For each={sync.data.lsp}>
{(item) => (
<text fg={item.status === 'connected' ? theme.success : theme.error}>
•
</text>
<text>{item.id} {item.root}</text>
)}
</For>
4. Todo 区
显示未完成的任务:
<For each={todo()}>{(todo) => <TodoItem status={todo.status} content={todo.content} />}</For>
TodoItem 组件:
export function TodoItem(props: { status: "pending" | "in_progress" | "completed"; content: string }) {
// 显示图标:⭕ (pending), ⏳ (in_progress), ✓ (completed)
// 删除线显示已完成任务
}
5. Modified Files 区
显示已修改的文件和差异统计:
<For each={diff()}>
{(item) => (
<text>{item.file}</text>
<text fg={theme.diffAdded}>+{item.additions}</text>
<text fg={theme.diffRemoved}>-{item.deletions}</text>
)}
</For>
Home 组件
首页显示,用于开始新会话。
export function Home() {
return (
<box flexGrow={1} justifyContent="center" alignItems="center">
<Logo />
<Prompt ref={promptRef} hint={Hint} />
<Tips />
<Toast />
</box>
<box> {/* 状态栏 */ }
<text>{directory()}</text>
<text>{connectedMcpCount()} MCP</text>
<text>{Installation.VERSION}</text>
</box>
)
}
Logo 组件
显示 OpenCode Logo:
<text>
<span fg={theme.primary}>Open</span>
<span fg={theme.accent}>Code</span>
</text>
Tips 组件
显示使用提示,可隐藏:
const tipsHidden = createMemo(() => kv.get("tips_hidden", false))
const showTips = createMemo(() => {
if (isFirstTimeUser()) return false
return !tipsHidden()
})
提示内容:
- 使用
/访问命令 - 使用
@选择 agent - 使用
Ctrl+X打开命令面板
Dialog 组件
DialogProvider
对话框管理器,支持对话框堆栈。
export const { use: useDialog, provider: DialogProvider } = createSimpleContext({
name: "Dialog",
init: () => {
const [stack, setStack] = createStore<JSX.Element[]>([])
return {
replace(component: () => JSX.Element) {
setStack([component()])
},
clear() {
setStack([])
},
push(component: () => JSX.Element) {
setStack(produce((draft) => draft.push(component())))
},
pop() {
setStack(produce((draft) => draft.pop()))
},
}
},
})
常用对话框
| 对话框 | 用途 | 文件 |
|---|---|---|
| DialogCommand | 命令面板 | component/dialog-command.tsx |
| DialogModel | 模型选择 | component/dialog-model.tsx |
| DialogAgent | Agent 选择 | component/dialog-agent.tsx |
| DialogSessionList | 会话列表 | component/dialog-session-list.tsx |
| DialogStatus | 状态查看 | component/dialog-status.tsx |
| DialogThemeList | 主题选择 | component/dialog-theme-list.tsx |
| DialogHelp | 帮助信息 | ui/dialog-help.tsx |
| DialogConfirm | 确认对话框 | ui/dialog-confirm.tsx |
| DialogAlert | 警告对话框 | ui/dialog-alert.tsx |
DialogCommand
命令面板,提供模糊搜索的命令选择:
export function DialogCommand() {
const commands = useCommands()
return (
<dialog>
<textarea placeholder="Search commands..." onInput={handleInput} />
<For each={filteredCommands()}>{(cmd) => <item onClick={() => cmd.onSelect(dialog)}>{cmd.title}</item>}</For>
</dialog>
)
}
特性:
- 模糊搜索命令
- 分类分组
- 快捷键提示
- Leader 模式触发(Ctrl+X)
Toast 组件
显示临时提示消息。
export function Toast() {
const [toasts, setToasts] = createSignal<ToastItem[]>([])
return (
<For each={toasts()}>
{(toast) => (
<box>
<text>{toast.title}</text>
<text>{toast.message}</text>
</box>
)}
</For>
)
}
API:
export const toast = useToast()
toast.show({
title: "Success",
message: "操作成功",
variant: "success", // success | error | warning | info
duration: 3000,
})
toast.error("错误消息")
toast.success("成功消息")
toast.warning("警告消息")
工具组件
Logo
显示品牌 Logo:
export function Logo() {
const { theme } = useTheme()
return (
<text>
<span fg={theme.primary}>Open</span>
<span fg={theme.accent}>Code</span>
</text>
)
}
Tips
显示使用提示:
export function Tips() {
const tips = [
{ key: "/", text: "access commands" },
{ key: "@", text: "select agent" },
{ key: "Ctrl+X", text: "command palette" },
]
return (
<For each={tips}>
{(tip) => (
<text>
<span fg={theme.primary}>{tip.key}</span>
<span>{tip.text}</span>
</text>
)}
</For>
)
}
Border
自定义边框样式:
export const SplitBorder = {
customBorderChars: {
...EmptyBorder,
vertical: "┃",
horizontal: "─",
topLeft: "┌",
topRight: "┐",
bottomLeft: "└",
bottomRight: "┘",
verticalLeft: "├",
verticalRight: "┤",
horizontalUp: "┴",
horizontalDown: "┬",
cross: "┼",
},
}