【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
标题
205、【Agent】【OpenCode】TUI 内部:.tsx 与 .ts 的分界线
背景
上篇 blog
【Agent】【OpenCode】TUI 内部:SDK 客户端与事件流
拆了 TUI 消费 thread.ts 服务的现场:app.tsx:144-150 把 transport(url/fetch/events/directory)交给 SDKProvider,context/sdk.tsx 用 createOpencodeClient 造出 client;事件来源按 internal/external 分叉(订阅 RPC 事件源 vs 走真实 SSE);事件先入队、16ms 窗口合并、batch() 一次渲染;切 workspace 重建 SDK。204 走进了 TUI 内部,本篇再往下看一层文件系统层面的规律 :同样在 cli/cmd/tui 目录下,为什么 app.tsx 是 .tsx 而 thread.ts 是 .ts?这个后缀不是随手命名的
OpenCode
app.tsx 和 thread.ts 只差一个 x,含义却完全不同:.tsx = TypeScript + JSX 。这个差异决定了编译器怎么解读文件里的 <。
🧩 本质区别:JSX 解析
.ts 是纯 TypeScript,文件里的 < 一律当泛型 或比较运算符 。.tsx 开启了 JSX 解析------HTML 风格的标签语法(<组件>...</组件>)会被当成组件标记:
tsx
// app.tsx 里的 JSX:< 是标签
<SDKProvider url={input.url} directory={input.directory}>
<SyncProvider>...</SyncProvider>
</SDKProvider>
对比 thread.ts 里的 <------它是泛型参数,不是标签:
ts
// thread.ts 里的 <:是泛型
type RpcClient = ReturnType<typeof Rpc.client<typeof rpc>> // thread.ts:22
client.on<Event>("event", handler) // thread.ts:44
同一个 <,在 .tsx 里是标签、在 .ts 里是泛型------编译器靠后缀决定怎么解析。这就是后缀不能随便写的原因。
📊 TUI 目录的分布规律
实测 cli/cmd/tui 目录下 30+ 个文件,后缀与"层"严格对应:
| 后缀 | 层 | 典型文件 |
|---|---|---|
.tsx |
渲染/组件层 | app.tsx、context/*.tsx、component/*.tsx、routes/home.tsx、toast.tsx |
.ts |
纯逻辑层 | thread.ts、worker.ts、win32.ts、attach.ts、event.ts、directory.ts、clipboard.ts、selection.ts、editor.ts |
凡是"要渲染 UI、要进组件树 "的文件用 .tsx;纯逻辑、工具、后端文件 用 .ts。

🔬 判据:是"属于组件层",不是"字面含 JSX"
最容易被忽略的一点:.tsx 与否取决于文件在组件树里的角色,而不是它有没有写 JSX 标签。三个证据:
1. sdk.tsx 几乎没 JSX,却是 .tsx
ts
// sdk.tsx:11
export const { use: useSDK, provider: SDKProvider } = createSimpleContext({...})

它自身只有泛型 <...>,没有任何 JSX 标签------但它是组件树的提供者 (返回 SDKProvider 组件,被 app.tsx 放进树里),所以按 .tsx。
2. helper.tsx 的 createSimpleContext 生成真 JSX
tsx
// helper.tsx:10-15
provider: (props: ParentProps<Props>) => {
return (
<Show when={...}>
<ctx.Provider value={init}>{props.children}</ctx.Provider>
</Show>
)
}
这里有真 JSX (<Show>、<ctx.Provider>)------生成 provider 组件的工厂,必须 .tsx。
3. context/directory.ts 提供数据但不渲染,所以是 .ts
它给 TUI 提供目录上下文数据,却不产出任何组件 ,纯逻辑 → .ts。
⚠️ 别被骗:.ts 里的 <...> 是泛型
thread.ts:22 的 <typeof rpc>、thread.ts:44 的 <Event>、win32.ts:14 的 <typeof kernel>------这些都是泛型参数 ,不是 JSX。判断标准:泛型 <> 里是类型 ,JSX <> 里是组件或标签。
💡 TUI 用的是 Solid.js
app.tsx 的渲染入口是 Solid:
ts
// app.tsx:6
import { Switch, Match, createEffect, ErrorBoundary, ... } from "solid-js"
// app.tsx:132
render(
() => { return ( ...整棵组件树... ) },
{ targetFps: 60, exitOnCtrlC: false, ... },
)

JSX 是 React/Solid/Preact/Vue 等共享的模板语法,都用 .tsx。TUI 用的是 Solid.js ,它的响应式(signal/batch)也解释了 204 篇里"事件 batch() 一次渲染"的设计。

📊 总结
| 维度 | .tsx | .ts |
|---|---|---|
| 含义 | TypeScript + JSX | 纯 TypeScript |
< 的语义 |
组件标签 | 泛型 / 比较 |
| 属于 | 渲染/组件层 | 纯逻辑/工具/后端 |
| 典型 | app / context 组件 / component / route | thread / worker / win32 / clipboard |
📌 一句话记忆
.tsx是 TypeScript + JSX:编译器靠后缀决定<当标签还是泛型。TUI 里 .tsx 属于组件/渲染层(app、provider、component),.ts 属于纯逻辑层(thread、worker、win32);判据是"是否进组件树"而非"是否写了 JSX"------sdk.tsx 没 JSX 也 .tsx,directory.ts 提供数据不渲染所以 .ts。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog