Components 使用文档
本文档覆盖 @simon_he/vue-tui 当前内置的 Vue 组件,用于统一「渲染/参数/事件」的契约,便于实现一致的验收与测试。
坐标/尺寸单位:所有
x/y/w/h均以「cell(字符格)」为单位,而不是像素。
完整的 Props/Events 列表请以自动生成文件为准:
docs/generated/components-api.md(运行pnpm run docs:gen生成)。
导入入口
| API maturity | Import | 组件 |
|---|---|---|
| Public | @simon_he/vue-tui | TerminalProvider TBox TCommandPalette TDataTable TDialog TInput TLink TLinkifyText TList TSelect TTable TText TTree TView form helpers 和 TBadge/TTag/TDivider/TCode |
| Advanced | @simon_he/vue-tui/vue | TAnchor TDebugOverlay TFlex TFlexItem TFlow TForm TInputBox TJsonEditor TMermaid TMermaidText TMultilineModal TPathPicker TProgress TSpinner TSplitPane TTabs TToastViewport TRenderLayer TRenderPlane TRouterView TTransition 和 overlay/navigation/status helpers |
| Public | @simon_he/vue-tui/markdown | TMarkdownText TVirtualMarkdown |
| Public | @simon_he/vue-tui/mermaid | TMermaid TMermaidText TBeautifulMermaidText beautifulMermaidRenderer createBeautifulMermaidRenderer |
| Experimental | @simon_he/vue-tui/experimental | TCandlestickChart TContributionGraph TLineChart TPieChart TVirtualList TTranscriptView TLogView TLogSearchBar TLogSearchResults TLogSearchPager TLogLinksPanel TLogVirtualSearchResults TLogVirtualLinksPanel TLogScrollbar TLogMinimap |
| Experimental | @simon_he/vue-tui/agent | TAgentTerminalGraphic TAgentTranscript TMermaid TMermaidText TThinkingView TUserMessageView TToolCallView TToolLogView TVirtualMarkdown TVirtualList TRenderPlane 和 agent/console 常用基础组件 |
| Experimental | @simon_he/vue-tui/agent/mermaid | TMermaid TMermaidText TBeautifulMermaidText beautifulMermaidRenderer createBeautifulMermaidRenderer |
下面的组件速读按用途分组,不代表 root entrypoint 导出。每个组件的 primary import 以生成的 组件 API 为准。
组件速读
| 类别 | 组件 | 典型用途 | 适配性判断 |
|---|---|---|---|
| Root | TerminalProvider | 创建 terminal / renderer / event manager 上下文 | 通用,适合所有宿主 |
| Layout | TBox TView TAnchor TFlex TFlexItem TFlow TRenderLayer TRenderPlane | 布局、裁剪、层级、分层组合 | 通用,和 CLI 业务无关 |
| Text / Action | TText TLink TLinkifyText TMermaid TMermaidText TBadge TTag TDivider TCode TKeyHint TTransition | 文本渲染、链接操作、结构图、徽标、分隔符 | 通用 |
| Input / Form | TInput TInputBox TAutocompleteInput TCheckbox TFormField TPasswordInput TRadioGroup TSlider TSwitch TJsonEditor | prompt、表单、结构化文本编辑 | 通用,但推荐把补全/校验放到插件层 |
| Data / Tree | TTable TDataTable TTree | 多列数据、排序过滤、层级选择 | 通用 |
| Charts | TContributionGraph TLineChart TCandlestickChart TPieChart | 贡献热力图、趋势、K 线、占比图 | 通用,适合 dashboard / agent token usage |
| Pickers | TCommandPalette TList TVirtualList TTranscriptView TLogView TLogSearchBar TLogSearchResults TLogSearchPager TLogLinksPanel TLogScrollbar TLogMinimap TSelect TPathPicker | palette、列表、transcript、日志、路径选择 | TPathPicker 本体可复用,路径语义由 provider 注入 |
| Overlay | TDialog TContextMenu TPopover TTooltip TToastViewport TMultilineModal TDebugOverlay | 对话框、菜单、提示、toast、详情查看、调试覆盖层 | 通用,适合多种宿主 |
| Navigation | TBreadcrumb TStatusBar TTabs TSplitPane TRouterView + createTerminalRouter() | 路径导航、状态栏、多页面 TUI / shell | 通用 |
| Agent Chrome | TAgentTerminalGraphic TThinkingView TUserMessageView TToolCallView | terminal graphics、thinking/user/tool-call chrome | 默认对齐 best-agent 风格,可通过 style props 覆盖 |
如果你更关心“哪些地方还应该继续做插件化”,建议配合阅读:扩展性与插件化。
如果你在实现完整 CLI surface、dialog/input 交互、transcript 或高频渲染区域,建议先读:Terminal UI Best Practices。
基础约定
Style(样式)
style 使用 Style(ANSI 风格语义):
fg/bg: ANSI 颜色名(例如whiteBright/blue等)bold/dim/italic/underline/inverse: 布尔开关
TerminalProvider 提供 defaultStyle 作为默认渲染样式;组件的 style 传入后会覆盖默认值(通常是整行/整块生效)。 未显式传入时,defaultStyle 仍是普通可变对象;如果要触发依赖它的组件重新绘制,推荐替换整个对象,而不是原地修改字段。
TThinkingView、TUserMessageView、TToolCallView 这种 agent/console 组件保留通用数据边界:只接收 title、content、status、collapsed、suffix、preview 等渲染语义,不接收 provider/session/tool schema。默认样式对齐 best-agent CLI 的 transcript chrome;宿主可以用各组件的 style、headerStyle、contentStyle、titleStyle、suffixStyle、previewStyle 等 props 覆盖颜色。
zIndex(层级)
- 渲染层:同一 stack 内按
zIndex决定覆盖顺序(大者覆盖小者)。 - 事件层:可交互组件会注册到 EventManager,命中测试会偏向 更高 zIndex 或 更小面积 的节点。
事件(点击/键盘/焦点)
可交互组件遵循 Vue 事件命名习惯:
- 监听:
@click/@keydown/@focus/@blur… - v-model:
modelValue+update:modelValue
事件 payload 为终端事件(TerminalPointerEvent / TerminalKeyboardEvent),携带 cellX/cellY 等信息。
TerminalProvider
终端 UI 的根组件:创建 terminal、DOM renderer、EventManager、调度器(rAF)。
Props
cols(number, required): 终端列数rows(number, required): 终端行数defaultStyle(Style): 默认样式(默认{})autoResize(boolean): 是否根据容器尺寸自动 resize(默认false)minCols/minRows(number): autoResize 下最小尺寸recordEvents(fn?): 录制事件回调(用于 record/replay)selection(boolean | TerminalProviderSelectionOptions): 开启 terminal cell selection;鼠标松开时可自动复制;toast只影响TerminalProvider的复制提示 UIclipboard(ClipboardApi?): 给 selection auto-copy 注入 clipboard;不传时 browser 使用运行时 clipboardinputPlugins(TInputPlugin[]): 给子树里的TInput/TInputBox注入宿主插件(例如 terminal clipboard、TTY 风格快捷键);init-only,修改后需重新挂载 provider/inputpathPickerProvider(PathPickerProvider?): 给子树里的TPathPicker注入宿主路径 providerlinkOpener(TerminalLinkOpener | function?): 给TLink openMode="host"注入外部链接打开能力;浏览器TerminalProvider默认使用window.open,CLI/headless 需要通过createTerminalApp({ linkOpener })显式提供theme(TuiThemeOverrides?): 组件主题 token 覆盖;传入 partial overrides 即可,例如{ colors: { link: "blueBright" } },TerminalProvider会用createTheme()归一化;局部styleprops 仍然优先debugIme(boolean): 输出 IME 调试信息debugTrace(boolean): 开启 trace(commit/event/focus)domRendererOptions(DomRendererOptions?): DOM renderer 配置,例如syncFlushMaxRows/syncFlushCellBudget;link options 会在更新时刷新,其他选项按 mount-time 使用,修改后需重新挂载 provider
Slots
default: 渲染你的 TUI 组件树
Example
<TerminalProvider :cols="80" :rows="24" :default-style="{ fg: 'whiteBright' }">
<TBox :x="0" :y="0" :w="80" :h="24" title="Demo" border :padding="1">
<TText :x="0" :y="0" :w="78" value="Hello" />
</TBox>
</TerminalProvider>TText
渲染纯文本(可多行、可自动换行),并会对控制字符做清洗,避免写出组件矩形区域。
Props
x/y(number, required)zIndex(number)value(string, required)w/h(number?): 不传则按文本实际宽高推导style(Style?)clear(boolean): 每次 paint 是否先清空区域(默认true)wrap(boolean): 是否按w自动换行(默认false)depsKey(unknown?): 参与 render-node 依赖追踪的可选 key(用于强制 repaint)
Notes
wrap=true会保留显式\n为硬换行,并按 cell 宽度进行自动折行。- 多字节/宽字符(例如中文)按 cell 宽度计算,不会半个字符被截断。
TBox
绘制一个矩形容器(可选边框/标题/内边距),并为子节点提供 layout(clipRect + origin 偏移),支持 scrollX/scrollY。
Props
x/y/w/h(number, required)zIndex(number)border(boolean): 是否绘制边框(默认true)title(string): 标题(会被安全截断)padding(number): 内边距(会自动 clamp,避免把内容挤没)scrollX/scrollY(number): 内容滚动偏移(单位 cell)style(Style?)clear(boolean): 是否先清空区域(默认true)
Slots
default: 内容区子组件
Events
TBox 主要用于绘制与裁剪,但也会对其矩形区域注册 hover 事件:
@pointerenter / @pointerleave(含 Capture 版本)
TView
一个可交互的矩形“视口”节点:提供 layout(origin/clipRect)与事件(click/key/focus/blur…),支持 scrollX/scrollY。
Props
x/y/w/h(number, required)zIndex(number)scrollX/scrollY(number)focusable(boolean): 是否可获得焦点(默认false)selectable(boolean?): 是否允许文本选择(默认undefined由上层决定)autoFocus(boolean): 可见时自动聚焦(默认false)
Events
@click/@dblclick/@pointerdown/@pointerup/@pointermove/@pointerenter/@pointerleave/@wheel/@keydown/@keyup/@focus/@blur
同时支持对应的
Capture版本(例如@clickCapture)。
TLink
可点击、可聚焦、可键盘激活的单行链接组件。除 disabled / openMode="none" 外,它会把 DOM-safe href 写入 Style.href,因此 DOM renderer links 开启时可以得到原生 anchor,CLI/stdout renderer 可以继续输出 OSC8 hyperlink。
默认 openMode="host":点击或按 Enter 时会先 emit activate,再调用 TerminalProvider.linkOpener 或 createTerminalApp({ linkOpener }) 注入的 openExternal() 尝试打开;浏览器 TerminalProvider 默认使用 window.open,CLI/headless 不会默认执行系统命令。
openMode 语义:
host: emitactivate,阻止 DOM native anchor 默认行为,并调用linkOpenerevent: emitactivate,阻止组件处理的 DOM native anchor click,不调用linkOpener;仍会写入Style.hrefmetadata,terminal OSC8 或 browser context menu 仍可能暴露 href,如需完全不输出 link metadata 请使用nonenative: click emitactivate并允许 renderer/native link activation;keyboard emitactivate后在有linkOpener时作为 terminal focus fallback 打开;如果modifierClick不满足,会阻止 native clicknone: 只渲染文本,不写入 href metadata,不激活
TLink 接受 absolute https: / http: / mailto: 和 /docs、#section 这类 relative href;宿主应按自己的策略重新解析或拒绝 relative href。TLink 有意拒绝 file: URL;terminal-specific file: opt-in 只适用于底层 Style.href 写入者、stdout renderer 或 TLog retained index 这类显式 provider。
modifierClick="meta" 和 ctrlOrMeta 里的 Meta/Cmd 只对 browser/DOM 事件有意义;真实 CLI SGR mouse report 只携带 Shift/Alt/Ctrl,所以 CLI 下 ctrlOrMeta 等价于 Ctrl,meta 不会被真实鼠标输入满足。
domRendererOptions.links.activation="event" 面向 markdown/static rich text 这类直接写入 Style.href 的链接。它会在 DOM anchor 层先 preventDefault() 并调用 renderer-level onActivate;TLink 会尊重这个 defaultPrevented 状态,不再执行组件级 activate / host opener。组件化链接推荐由 TLink 自己拥有 activation。
Props
x/y(number, required)w(number?): 不传时按label || href的 cell 宽度计算h(number): 命中区域高度,默认1href(string, required)label(string?)style/hoverStyle/focusStyle/activeStyle(Style?)disabled(boolean)openMode('native' | 'host' | 'event' | 'none')activationKeys(string[]): 默认['Enter']modifierClick('none' | 'ctrl' | 'meta' | 'ctrlOrMeta')autoFocus(boolean)
Events
activate:{ href, label, source }open:{ href, label, source },host opener 返回 true 时触发,表示请求已被接受/尝试;不保证 OS 或 browser 实际打开了目标invalidHref:{ href, reason }click/keydown/focus/blur
<TLink
:x="2"
:y="4"
href="https://example.com"
label="Open example.com"
:focus-style="{ inverse: true }"
@activate="onLinkActivate"
/>TLinkifyText
自动识别纯文本里的 URL,并把匹配片段渲染成带 Style.href metadata 的文本。它不自己打开链接,也不注册点击事件;DOM renderer 是否生成 <a> 仍由 domRendererOptions.links 控制,CLI/stdout 是否输出 OSC8 仍由 stdout renderer 的 href sanitizer 控制。
默认只识别 http: / https: / mailto:。relative href 需要显式 opt in,避免把日志或路径文本误标成可打开链接;file: 不属于 public linkify 协议。
Public helper linkifyTextSegments("") returns an empty segment array; non-empty plain text with no links returns one plain { text } segment.
Props
x/y(number, required)w/h(number?)value(string, required)style(Style?)linkStyle(Style?): 默认{ fg: 'cyanBright', underline: true }clear(boolean)wrap(boolean)protocols(('http' | 'https' | 'mailto')[]?)allowRelative(boolean)maxUrlLength(number?)
<TLinkifyText :x="2" :y="6" :w="80" value="build failed: see https://example.com/docs" />TBadge
小型状态徽标,渲染为单行 [value] 文本。适合 tab badge、计数和短状态,不拥有交互或生命周期。
TTag
小型标签,渲染为单行 <label> 文本。tone 只影响语义色,不表示稳定性或权限边界。
TDivider
单行分隔符,可选 title。只负责绘制,不拥有折叠、分组或焦点行为。
TCode
单行 code 文本,用于命令、路径或短 token。它只做 cell 截断和样式渲染,不执行命令、不复制内容。
TMermaid / TMermaidText
Mermaid 组件详见下方 TMermaidText。这里仅作为 Text / Action 分类索引:基础组件从 @simon_he/vue-tui/vue 或 @simon_he/vue-tui/agent 导入并显式传 renderer;内置 beautiful-mermaid bridge 从 @simon_he/vue-tui/mermaid 或 @simon_he/vue-tui/agent/mermaid 导入,使用前需要安装 beautiful-mermaid。
TAgentTerminalGraphic
Agent terminal graphics 组件,从 @simon_he/vue-tui/agent 引入。它面向真实 stdout terminal:组件在 TUI buffer 中占用指定 cell rect,并通过 createStdoutRenderer() 注册的 terminal graphics output 在帧末尾写入 Kitty / iTerm2 / Sixel 等 raw escape payload。DOM/headless 或不支持图像协议时自动显示 fallback 文本;未传 fallback 时,kind="image" 默认为空文本,kind="math" 使用 content。
组件本身不 import KaTeX、不读取图片文件、不执行外部命令。宿主传入 renderer(content, context),根据 context.protocol 或 context.capabilities.preferredProtocol 返回 { type: "sequence", protocol, sequence, fallback?, clearSequence?, rows?, cols? } 作为可信 terminal image escape,或返回 { type: "text", text } 作为 Unicode/ANSI fallback。返回 null / undefined 会使用组件 fallback,不会进入错误态;renderer 抛错时同样显示 fallback,但会使用 errorStyle。为了避免 terminal escape injection,bare string 只会按普通文本 fallback 处理,不会作为 raw escape 写入 stdout。h 省略时会优先使用 renderer result 的 rows 推导占用高度,仍建议宿主显式传入最终 cell 高度。kind="math" 可用于 KaTeX/LaTeX,kind="image" 可用于普通图片。
虚拟滚动或重内容场景建议使用 deferRenderUntilVisible、suspendRenderWhileScrolling / suspended 和 createTerminalGraphicRenderQueue() 控制懒渲染、取消、缓存与并发。PNG 渲染链路可用 createPngTerminalGraphicRenderer() 组合 Kitty / iTerm2 序列;Sixel 需要宿主提供 toSixel encoder,例如 libsixel binding、img2sixel 包装或自定义 renderer。传给 PNG/Sixel renderer 的转换函数应观察 context.signal 并及时 settle;queue 会在 caller abort 后 reject,但并发 slot 会等实际转换 settle 后释放。默认 PNG cache key 覆盖 kind、尺寸、final、content/组件 cacheKey 和 renderer cacheSalt,不包含 protocol,因此使用默认 key 时 toPngBase64() 应保持协议无关;随 PNG frame 返回的 fallback 也会进入同一缓存,如果它依赖 protocol、主题、字体、DPR、KaTeX macro 或 renderer options,应传 cacheSalt、自行提供完整 cacheKey(),或改用 renderer 的 fallback() option。terminal 不支持图像、raw 不可见、组件不可见或缺少 Sixel encoder 时只会走 renderer-level fallback() 或组件 fallback,不会为了读取 PNG frame fallback 而执行 toPngBase64()。Kitty 支持显式 delete sequence;组件级自定义 Kitty clearSequence 只接受 context.imageId / context.placementId 对应的 d=i / d=I delete,current-cell d=c / d=C 不会作为组件 clear 使用。iTerm2 inline image 和 Sixel clear 是 best-effort,通过重绘占用的 cell rect 清理。自定义 Kitty renderer 如果依赖组件默认 clear,应使用 context.imageId / context.placementId 创建 draw sequence;否则应返回同一组 id/placement 的可靠 clearSequence。
真实终端 smoke checklist:
- [ ] Kitty native
- [ ] iTerm2 inline image
- [ ] tmux passthrough
- [ ] Sixel-capable terminal
- [ ] unsupported / CI / non-TTY fallback
<TAgentTerminalGraphic
:x="0"
:y="2"
:w="72"
:h="12"
kind="math"
content="\\frac{a}{b}"
fallback="a / b"
:renderer="terminalMathRenderer"
/>TCommandPalette
命令面板组件,组合 TDialog、TInput 和列表行渲染。默认按 label / detail / keywords 做 substring 过滤,也支持 v-model:query、custom matcher、async itemsProvider、group/separator 行和 closeOnSelect。select payload 包含 { item, index, sourceIndex, query, source },其中 index 是过滤/渲染后的 row index,sourceIndex 是原始 items 或 provider result index;默认选择命令不会自动关闭面板,宿主可在 @select 中更新 v-model,或显式开启 closeOnSelect。itemsProvider rejection 会渲染错误行并 emit loadError。
<TCommandPalette
v-model="paletteOpen"
:items="commands"
title="Command"
placeholder="Search commands"
@select="runCommand"
/>TTable
TTable 是多列静态表格,负责列宽、表头、可选边框和 row click。它只从 rows 顶部按当前高度取可见行,不内置 scrollTop、分页或 virtualization;大数据浏览应组合外部分页/offset,或使用 TVirtualList 一类窗口化视图。
TDataTable
TDataTable 在 TTable 上增加受控排序、过滤、行选择和受控 viewport offset;点击表头会 emit sortChange / update:sortBy / update:sortDirection。scrollTop 表示过滤/排序后结果集里的顶部可见行。rowSelect payload 里的 index 是当前 viewport 内的 visible row index,dataIndex 是过滤/排序后结果集里的绝对 index,originalIndex 是输入 rows 数组里的 index。rowKey 函数收到的 index 是原始 rows index,排序/过滤后仍保持行 identity。列 format(value, row, index) 里的 index 也是原始 rows index,过滤和渲染阶段保持一致。列 format 会影响显示和过滤匹配,排序使用 row[sortBy] 的原始值。键盘移动的 active row 使用 activeStyle,已提交选择继续使用 selectedStyle。它仍然是 non-virtual:rows 会在内存中排序/过滤,然后只把当前 visible slice 传给 TTable。
<TDataTable
:x="0"
:y="0"
:w="80"
:h="12"
:columns="columns"
:rows="rows"
row-key="id"
sortable
filterable
selectable
/>TContributionGraph
贡献热力图组件,按列优先顺序把 values 映射到固定行数的 cell 网格。默认是 7 行、列间留 1 cell 间隔,适合 GitHub contributions 风格、agent token usage、每日/每轮计数密度这类紧凑历史视图。
<TContributionGraph :x="0" :y="0" :values="dailyTokens" :rows="7" />TLineChart
单色终端折线图组件,把 values 采样到给定 w/h 区域内,用 box-drawing glyph 绘制阶梯折线。适合 token 总量、延迟、吞吐等小型 dashboard 曲线。
<TLineChart :x="0" :y="8" :w="40" :h="8" :values="tokenTotals" />TCandlestickChart
K 线图组件,接收 { open, high, low, close } 数组并在宽度不足时保留最新 candles。默认上涨为绿色、下跌为红色,可通过 upStyle、downStyle、wickStyle 覆盖。
<TCandlestickChart :x="0" :y="17" :w="50" :h="10" :candles="prices" />TPieChart
终端饼图组件,按 values 从顶部开始顺时针分段,用 segment styles 给不同区域着色。适合展示 token 分类、状态占比或简单资源分布。
<TPieChart :x="44" :y="8" :w="16" :h="8" :values="[prompt, completion]" />TTree
TTree 渲染层级节点,expandedIds 和 selectedId 都是受控状态。默认点击或 Space/Enter 可展开节点会 toggle;开启 selectableParents 后,点击 marker toggle,点击 label 或按 Enter select 父节点,Space 继续 toggle。
TCheckbox
checkbox 控件,使用 modelValue / update:modelValue,Space / Enter / click 切换。
TRadioGroup
radio group 控件,使用 options 和受控 modelValue 渲染单选列表。
TSwitch
switch 控件,适合二元配置开关。
TSlider
slider 控件,使用 min / max / step 和 ArrowLeft / ArrowRight 调整数值。
TFormField
TFormField 统一 label、help、error、required、disabled 的展示边界,不内置验证系统。 style 会作为 label、help、error 的基础样式;disabled 只让 label 变暗,slot 内容是否禁用由宿主组件控制。
<TFormField :x="0" :y="0" :w="44" :h="3" label="Token" help="Paste your API token" :error="error">
<TPasswordInput v-model="token" :x="0" :y="0" :w="40" />
</TFormField>TPasswordInput
TPasswordInput 是 TInput secret 的轻量包装,输入值仍由宿主通过 v-model 控制,渲染时隐藏明文。
TAutocompleteInput
TAutocompleteInput 组合 TInput 和 suggestions 列表。静态 suggestions 保持宿主受控,也可以用 suggestionProvider(query, { signal }) 走异步路径;provider rejection 会渲染错误行并 emit loadError。选择 suggestion 时 emit update:modelValue、change 和包含 { value, index, sourceIndex, option, query, source } 的 select,不 emit input。
TForm
Advanced 表单上下文组件,从 @simon_he/vue-tui/vue 引入。它提供 validation/error context:model、同步 rules、submit、validation,以及 TFormField name 对错误文本的读取。自定义字段可以通过 /vue 的 useTForm() 或 TFormContextKey 消费上下文。validate() 会在 submit 或宿主显式调用时更新 errors,不会在 model / rules 变化时自动重新校验。TForm 通过 template ref 暴露 validate()、submit()、clearValidation() 和 setFieldError()。disabled 会通过 form context 禁用当前已接入的内置表单控件(TCheckbox、TSwitch、TRadioGroup、TSlider);TInput、TPasswordInput、TAutocompleteInput 仍需要宿主显式处理禁用语义。readOnly 目前只是通过 form context 提供给自定义字段消费者的提示,不会自动让后代输入控件只读。它不规定 schema 格式,也不拥有远程提交。
TContextMenu
TContextMenu 是轻量菜单 overlay,基于现有 TBox / TText / TView 渲染。默认会在外部点击时关闭;如需保持 non-modal 行为,可设置 closeOnOutside=false。x / y / w 是 caller-owned placement,组件不会自动贴边 clamp 或 flip。它不会直接操作系统 clipboard 或浏览器窗口;菜单项动作通过 select 交给宿主处理。
<TContextMenu
v-model="open"
:x="cursor.x"
:y="cursor.y"
:items="[{ id: 'open', label: 'Open Link' }]"
@select="handleMenuSelect"
/>TPopover
TPopover 是带边框的轻量内容浮层,可以传 content,也可以用 default slot 自定义内容。x / y / w / h 是 caller-owned placement,组件不会自动贴边 clamp 或 flip。
TTooltip
TTooltip 是单行提示文本,适合说明 unfamiliar controls 或链接打开条件。
TToastViewport
Advanced toast 视口,从 @simon_he/vue-tui/vue 引入。它只渲染传入的 items,不会创建全局 singleton,也不会自己启动计时器;宿主负责 duration、dismiss 和队列状态。
placement 基于当前 layout / clipRect 放置 toast stack;offsetX / offsetY 是相对所选角的 cell inset,因此组件可以放在 TView、TBox 或 split pane 子树里。w 是单个 toast item 的宽度;当父 layout 没有 clipRect 时,用 viewportW / viewportH 指定用于 placement 的 viewport 尺寸。
TProgress
Advanced determinate progress,渲染 value / max、可选 label 和百分比。它不启动动画,适合 stdout/headless 快照。
TSpinner
Advanced indeterminate spinner,通过 frameIndex 选择帧。组件不会无条件开启 interval;宿主按自己的 scheduler 或 tick 更新 frameIndex。
TStatusBar
TStatusBar 用于 terminal app 的底部状态栏。它是纯渲染组件,不注册全局快捷键。
<TStatusBar :x="0" :y="23" :w="80" left="Ready" center="main" right="Ctrl+K" />TBreadcrumb
TBreadcrumb 渲染路径导航,点击 item 只 emit select。
<TBreadcrumb :x="0" :y="0" :w="60" :items="pathSegments" @select="goToPath" />TKeyHint
TKeyHint 渲染快捷键提示,不绑定或监听快捷键。
<TKeyHint :x="62" :y="0" combo="Esc" label="Close" />TTabs
Advanced tab header,从 @simon_he/vue-tui/vue 引入。它只管理 header 选择和 activeKey,不拥有 pane 内容。
TSplitPane
Advanced pane group,从 @simon_he/vue-tui/vue 引入。sizes 是受控的 cell size;空间不足时按 minSizes 压缩,空间有剩余时补到最后一个 pane。separator 支持键盘微调;default slot 接收 { panes },pane 内容由宿主按返回 rect 渲染。
Theme Tokens
createTheme() 生成完整主题对象;TerminalProvider.theme 通常直接接收 partial overrides,例如 { colors: { link: "cyanBright" } },provider 会自行归一化。主题 token 只提供默认样式;组件局部传入的 style、hoverStyle、focusStyle 等 props 会覆盖主题。
const theme = createTheme({
colors: {
link: "cyanBright",
linkVisited: "magentaBright",
danger: "redBright",
},
components: {
TLink: {
underline: true,
hoverUnderline: true,
},
},
});TAnchor
类似 TView,但用「定位约束」描述矩形:left/top/right/bottom/w/h。用于做相对定位(例如贴右/贴底的浮层)。
Props
left/top/right/bottom(number?)w/h(number?)zIndex(number)focusable(boolean)selectable(boolean?)
Events
@click/@dblclick/@pointerdown/@pointerup/@pointermove/@wheel/@keydown/@keyup/@focus/@blur
同时支持对应的
Capture版本(例如@clickCapture)。注:
TAnchor当前不提供@pointerenter/@pointerleave(需要 hover 事件时请用TView)。
TFlow
按方向把 items 映射成若干子视口(每个子项一个 TView),用于列表式布局(更偏 layout 工具)。
Props
x/y/w/h(number, required)items(unknown[], required)direction('vertical'|'horizontal')(默认vertical)gap(number):子项间隔(cell)itemSize(number):子项主轴尺寸(cell)zIndex(number)
Slots
item:({ item, index }) => VNode
TFlex / TFlexItem
用 Flexbox 风格约束声明 row/column 布局,最终仍生成 TView 子视口。适合 header/content/footer、左右栏和需要在 resize 后按 grow/shrink 重新分配 cell 的布局。
TFlexItem 的 width/height、w/h、basis、minWidth/minHeight、maxWidth/maxHeight 支持 cell 数值或百分比字符串。百分比会先扣除 TFlex padding;main-axis 百分比再扣除 gap 后计算,cross-axis 百分比按 content box 交叉轴计算。
wrap 会按 item 的显式主轴尺寸、basis 和 min/max 约束分行或分列;当前不读取 slot 内容固有尺寸。
rowGap/columnGap 可覆盖 gap 在对应轴上的值。alignContent 只影响 wrap 后的多行/多列交叉轴分布。
paddingX/paddingY/paddingTop/paddingRight/paddingBottom/paddingLeft 可覆盖 padding。TFlexItem 支持 margin、marginX/marginY 和四个方向的 margin;margin 参与主轴分配和 wrap 判断,最终子内容仍渲染在 margin 内侧。
TFlexItem.order 控制视觉排列顺序;相同 order 的 item 保持原 slot 顺序。
需要内容固有尺寸时,可以在 TFlexItem.measure 中返回 preferred width/height。measure 会收到扣除 padding 后的 maxWidth/maxHeight 和当前 direction,返回值会在没有显式尺寸或 basis 时参与布局。
Props
TFlex:x/y/w/h、direction、gap、rowGap、columnGap、padding、paddingX/paddingY、paddingTop/paddingRight/paddingBottom/paddingLeft、wrap、alignItems、justifyContent、alignContent、zIndexTFlexItem:grow、shrink、basis、width/height、w/h、minWidth/minHeight、maxWidth/maxHeight、measure、order、zIndex、margin、marginX/marginY、marginTop/marginRight/marginBottom/marginLeft、alignSelf
Slots
TFlexItem.default:({ rect }) => VNode
TInput
单行文本输入框(含光标、选择、剪贴板、IME 组合输入、快捷键),通过 v-model 管理值。
Props(常用)
x/y/w(number, required)h(number):高度(默认1)modelValue(string)+update:modelValueplaceholder(string?)placeholderWhenFocused(boolean):聚焦时是否显示 placeholder(默认false)autoFocus(boolean)cursorBlink(boolean)cursorShape('block'|'underline'|'bar')style(Style?)secret(boolean)/maskChar(string):密码模式plugins(TInputPlugin[]):输入增强插件(见下方);init-only,修改后需重新挂载TInput
TInput功能较多,完整参数以源码为准:src/vue/components/TInput.ts。跨宿主注意:
TInput本体已经开始把 terminal clipboard、TTY 判定、路径 href 这类宿主行为往 plugin 边界迁移。现在更推荐通过TerminalProvider.inputPlugins、createTerminalApp({ inputPlugins })或局部plugins注入宿主能力,而不是继续把平台差异写死到组件里。像 copy toast 这种 UI 反馈也应由宿主显式提供,不再依赖默认全局 hook。
Events(补充)
input/change/keydown/focus/blurupdate:mentions/mentionClick:collectMentions=true时(见createPromptMentionPlugin())update:multilineTexts/multilineClick:多行 token 相关validationError:文本过滤/校验插件上报
TInput Plugins
用于扩展 TInput 的输入体验:通过 :plugins="[...]" 注入。插件列表是 init-only;如果需要切换宿主插件、path provider 或 prompt plugin,请重新挂载对应的 TInput。
宿主级插件也可以统一从上层注入:
TerminalProvider.inputPluginscreateTerminalApp({ inputPlugins })createTerminalApp({ clipboard }):只需要接入 clipboard 时的简化入口;传入inputPlugins时仍由宿主完全控制插件组合
TerminalProvider.inputPlugins 也是 init-only;已经挂载的 TInput 不会重新安装插件列表。
createTInputHostPlugin()
把宿主能力封装成 TInput 插件。适合注入:
readClipboardText/writeClipboardTextshowToastresolvePath/pathToHrefisTerminalLike
@simon_he/vue-tui 导出 browser-safe 的 createTInputHostPlugin()。CLI 侧的 defaultTInputHostPlugin 和 createDefaultTInputHostAdapter() 从 @simon_he/vue-tui/cli 导出,负责 Node-like 的 clipboard / path 行为,不会自动附带 UI toast。如果宿主希望保留 Copied / Copy failed 这类提示,需要显式提供 showToast。
createOsc52ClipboardProvider() 可作为 terminal clipboard 写入 provider 显式传给 createTerminalApp({ clipboard })。它不会默认执行系统剪贴板命令。
一个最小宿主接线示例:
import { createTInputHostPlugin } from "@simon_he/vue-tui";
import { createDefaultTInputHostAdapter } from "@simon_he/vue-tui/cli";
import { createTerminalApp } from "@simon_he/vue-tui/cli";
const baseHost = createDefaultTInputHostAdapter();
const app = createTerminalApp({
cols: 80,
rows: 24,
component: App,
inputPlugins: [
createTInputHostPlugin({
...baseHost,
showToast(message) {
toastStore.show(message);
},
}),
],
});createPromptMentionPlugin()
提供 prompt/mention 的浮层补全:
- prompt:基于
promptSuggestions+promptTrigger(默认/)匹配并弹出列表 - mention:基于
mentionTrigger(默认@)匹配;配合collectMentions=true会把选择结果写入mentions(通过update:mentions) - mention 数据源既可以来自
mentionSuggestions/mentionSuggestionProviders,也可以通过mentionPathProvider注入路径补全能力 - 如果宿主就是 Node / 本地文件系统语义,可以直接用
createNodeMentionPathProvider() - 键盘:
↑/↓选择,Tab/Enter接受,Esc关闭浮层
createTextRestrictionPlugin({ rules })
注册输入过滤规则(allow/deny/replace/filter),用于限制字符集、替换非法字符或做整体校验。
- 命中过滤/拒绝时会触发
TInput的validationError事件(payload 含originalText/acceptedText等)
TInputPluginsContextKey
高级宿主如果希望按子树组合/覆盖输入插件,可以直接用公开导出的 TInputPluginsContextKey 做 provide/inject。
- 适合像 reference app 那样,在某个页面根节点统一补一个局部 host plugin
- 比起给每个
TInput单独传plugins,更适合做“页面级宿主接线”
TInputBox
带边框的输入框组合组件(内部是 TBox + TInput)。
Props(常用)
x/y/w/h(number, required)modelValue(string)+update:modelValuetitle(string?)placeholder(string?)autoFocus(boolean)plugins(TInputPlugin[])
TList
可滚动列表(单选):支持点击/双击、键盘导航、滚轮滚动,v-model 维护选中 index。
TList 适合小数据选择器。大数据选择/浏览场景请使用 TVirtualList,日志、streaming transcript、append-only output 场景请使用 experimental TLogView,避免把大数组直接传进 Vue deep reactivity。
Limitation: TList wheel optimization coalesces bursts and repaints viewport rows; it does not reuse shifted rows or repaint only exposed rows. Large datasets should use
TVirtualList/TLogView.
Props
x/y/w/h(number, required)items(string[], required)itemVersion(number):同长度内容更新时可递增,触发 repaintmodelValue(number)+update:modelValuestyle(Style?)autoFocus(boolean)closeOnBlur(boolean)
Events
change:{ index, value }scroll:scrollTop(number)close/focus/blur/keydown
Wheel scrolling and selection
Behavior change for the wheel-mailbox release:
TListwheel is viewport-only.TListwheel no longer updatesmodelValue.TListwheel no longer moves the active selection.TListwheel burst applies only the finalscrollTopin the next frame and emitsscrollat most once per frame.TListtreatsupdate:modelValueas selection-change, not selection-confirm.- Enter and double click emit
change; they do not emitupdate:modelValuewhen committing the already-active item. - Keyboard-driven and external-model-driven viewport changes no longer emit
scroll. TListscrollnow represents viewport-driven scroll changes, especially wheel scrolling and programmatic clamp.onScrollis a result notification, not a veto/cancel hook.- Same-length item text changes require replacing the
itemsarray reference or bumpingitemVersion.
TList treats wheel scrolling as viewport-only. Wheel scrolling emits scroll, but it does not update modelValue and does not move the active selection. Keyboard navigation, click, double click, and Enter reattach selection to the visible viewport. Each applied TList wheel scroll still repaints the visible viewport; exposed row-only slow scrolling remains a TVirtualList / TLogView / renderer follow-up. If existing code depended on wheel scrolling to update modelValue, listen to scroll instead. If selection should follow scroll, synchronize that explicitly from onScroll; onScroll is a result notification, not a veto/cancel hook.
Migration example:
<TList
:items="items"
:model-value="selectedIndex"
@update:model-value="selectedIndex = $event"
@scroll="viewportTop = $event"
@change="confirmItem"
/>Before this release, wheel scrolling could move selectedIndex. After this release, wheel scrolling only updates viewportTop; keyboard navigation and click still update selectedIndex, while Enter and double click call confirmItem.
TList uses the same full-rect clipping model as TText/TVirtualList: when the list is clipped from the top or left, paint and hit testing keep the source row/column offset instead of rebasing the clipped area to a new viewport origin. x/y/w/h are cell coordinates. Fractional geometry is normalized by flooring the start and end cell edges; pass integers for deterministic layout. When changing styles, replace the style object instead of mutating it in place. Replace the items array reference when item text changes without changing length, or bump itemVersion. For large mutable data sources, prefer TVirtualList with itemVersion.
scroll(top) represents viewport-driven scroll changes, not every internal viewport-top mutation.
scroll(top) is emitted when:
- wheel scrolling changes the viewport top
- item-count or clipped-viewport changes programmatically clamp the viewport top
Hidden v-show=false lists still emit scroll(top) when item-count changes force a real internal scrollTop clamp, but they do not dirty terminal rows or commit visible output. A fully clipped viewport keeps detached scrollTop without clamping to 0 until a finite viewport height is restored. If a wheel frame is still pending when the viewport becomes hidden or fully clipped, that pending wheel is canceled and does not emit scroll.
scroll(top) is not emitted when:
- keyboard selection calls
ensureActiveVisible() - external
modelValuesynchronization callsensureActiveVisible() - click / double click selection calls
ensureActiveVisible()
Terminal-level DOM wheel handling may still prevent browser page scrolling even when TList itself does not consume an edge wheel event. Handler-level preventDefault() here only describes whether the list consumes the wheel internally.
Selection event semantics
TList treats update:modelValue as a selection-change event, not a selection-confirm event.
- Arrow / Home / End / PageUp / PageDown emit
update:modelValueonly when the active index changes. - Click emits
update:modelValueonly when the clicked index differs from the current active index. - Enter and double click emit
change. - Enter and double click do not emit
update:modelValuewhen they commit the already-active index.
scroll is emitted synchronously after internal viewport state changes. If an onScroll handler synchronously mutates modelValue or replaces items, TList may render the viewport change first and reconcile controlled props on the next Vue tick; onScroll is not a synchronous veto point for wheel scrolling.
Mutating style in place does not schedule repaint by itself. Replace the style object, or rely on a later repaint-triggering interaction if you choose to mutate it in place. Derived active/dim style caching only applies to stable frozen style objects and the internal empty-style cache; mutable non-empty style objects favor correctness over allocation reuse.
Detached wheel state is reattached only when selection changes, external modelValue changes, click/double-click/Enter/keyboard navigation happens, or data/geometry clamps require it. A parent re-render with the same modelValue does not reset the viewport. Reattaching detached state by itself does not request repaint; repaint only happens when active rows or scrollTop change.
TVirtualList
大数据选择/浏览列表:使用 itemCount / itemVersion / getItem 从外部数据源读取可见行,避免把大数组本体放进 Vue deep reactivity。它不是日志/streaming 组件;append-only 输出请使用 TLogView。
Phase 1 experimental API:当前从
@simon_he/vue-tui/experimental导出,暂不进入 root 入口。API 仍可能在 overscan、TLogView 等后续能力落地前调整。
Props
x/y/w/h(number, required)itemCount(number, required)itemVersion(number, required):数据变更版本号getItem((index: number) => unknown, required)renderItem((item, index) => unknown)modelValue(number)+update:modelValuescrollTop(number?)+update:scrollTop:受控 viewport scrollTop;省略时由组件内部维护style/activeStyle(Style?)autoFocus(boolean)rowScrollMode("off" | "unsafe-full-row"):实验性 unsafe wheel 优化;当前TVirtualList仅在 terminal/headless(非 DOM rows)路径、组件占满整行、未被裁剪、renderer 支持scrollOperations、且调用方确认组件独占这些 plane rows 时使用 exposed-row row-scroll;DOM 路径仍保持 viewport repaint,否则保持默认"off"
Data source
getItem 和 renderItem 应保持稳定引用,数据变化用 itemVersion 通知组件。style / activeStyle 对象也应按 immutable 方式使用;样式变化时替换对象 identity。
const items = markRaw(bigArray);
const itemVersion = ref(0);
const getItem = (index: number) => items[index];
const renderItem = (item: Row) => item.title;避免在模板里传 inline function:
<TVirtualList :get-item="(index) => items[index]" />Wheel Scroll
Wheel burst 通过 frame mailbox 合并;同一帧只应用最后的 scrollTop。滚动 repaint 只调用 markDirtyRows(viewportRows),dirty rows 不超过可见 viewport 高度,也不会走 exposed-row fast path 或提交 scrollOperations。组件 hidden、fully clipped、unmount 或受控 scrollTop 在 RAF 前变化时,会取消 pending wheel,不会让旧 wheel 覆盖新的受控位置。
Selection model
modelValue 使用 optimistic controlled 语义:键盘和点击会先更新组件内部 active row 并 emit update:modelValue。如果父组件稍后接受、延迟应用或改成其它 modelValue,组件会在 prop 同步时跟随;如果父组件完全忽略 update,组件会保留本地 optimistic active state。
Events
change:{ index, value }update:scrollTop:scrollTop(number)scroll:scrollTop(number)focus/blur/keydown
TTranscriptView
Transcript row viewport:渲染 message / action / tool-call / approval rows,支持 row-scoped action/link hit regions、focus navigation、cell selection copy 和 wrapped visual rows。
Experimental prototype:当前从
@simon_he/vue-tui/experimental导出,暂不进入 root 入口。它会在当前 layout state 中 flatten source rows 到 visual rows;适合小到中等 transcript 和交互原型,建议控制在 few thousand visual rows 量级,不适合作为几十万 visual rows 的高吞吐 retained transcript 视图。大规模 append-only output 继续使用TLogView。
Props
x/y/w/h(number, required)source(TTranscriptDataSource, required):提供rowCount()、getRow(index),可选提供getRowKey(index)、getRowVersion(index)、firstRowIndex()version(number, required):数据变化版本号scrollTop(number?)+update:scrollTopdefaultScrollTop(number?)autoStickToBottom(boolean)selectable(boolean)wrap(boolean)style/hoverStyle/focusStyle(Style?)autoFocus/focusable/wheelScroll(boolean)keyboardRegions(boolean):默认true,获得焦点时Tab/Shift+Tab在当前 viewport 的 hit regions 间循环 focus,Enter激活 focused region,Escape清除 focus
Events
actionClick/linkClick/foldToggle/toolClick:{ region, row, rowIndex, absoluteRowIndex, event }rowClick:{ row, rowIndex, absoluteRowIndex, event }hoverRegion: region event ornullscroll: scroll metricsupdate:scrollTop:scrollTop(number)
TTranscriptSegment.text 是 inline-only 文本;显式 \n / \r / \t 会按 inline cell 文本规整。需要保留显式换行时,请在 source 层拆成多个 transcript rows,或拆成独立 visual row blocks。
TMarkdownText
Markdown renderer for static or streaming text content。它走独立的 parser -> block -> visual row -> paint 链路,不会把 Markdown AST 直接交给 TText。
Markdown import:
@simon_he/vue-tui/markdown
contentstring 路径仍然只做 per-frame coalescing:一帧内多次 append 会合并成一次 rebuild,但 rebuild 本身仍然会从当前 full markdown string parse。长文档 streaming transcript 场景可以使用createMarkdownBlockSource(),在消息、tool fence 或代码块完成时finalizeBlock(),再把blocks传给TVirtualMarkdown,避免反复重 parse 已 finalize 的历史。
TMermaid
TMermaid 是 TMermaidText 的短别名,适合 agent console 里直接展示 Mermaid flowchart / sequence / state diagram 的 terminal text 输出。
Advanced import:
@simon_he/vue-tui/vueAgent import:
@simon_he/vue-tui/agentBeautiful Mermaid bridge import:
@simon_he/vue-tui/mermaidAgent Beautiful Mermaid bridge import:
@simon_he/vue-tui/agent/mermaid
注意:
@simon_he/vue-tui/vue和@simon_he/vue-tui/agent导出的TMermaidText是 renderer-agnostic primitive,不会自动 importbeautiful-mermaid。需要安装依赖后零配置使用内置 renderer 时,请从
@simon_he/vue-tui/mermaid或@simon_he/vue-tui/agent/mermaid导入TMermaidText/TMermaid。
@simon_he/vue-tui/mermaid和@simon_he/vue-tui/agent/mermaid的内置 beautiful-mermaid wrapper 默认使用 size guard + simple-flowchart-only guard;final=true后仅对简单 flowchart 尝试渲染。renderer 成功时原子替换为渲染结果;复杂 Mermaid、大 Mermaid、renderer 失败、超时或返回空白时保持源码显示。
@simon_he/vue-tui/vue和@simon_he/vue-tui/agent导出的 renderer-agnostic primitive 不会默认拦截复杂 Mermaid;如果需要限制 custom renderer,只传入shouldRenderSource={isSimpleMermaidFlowchartSource}或自定义 guard。注意:Markdown 里的
mermaidcode fence 当前只保留code_block.languagemetadata,尚未自动走TMermaidText的 async render/cache 路径。
TMermaidText
Mermaid terminal text primitive。组件本身不直接依赖 beautiful-mermaid,因此可以安全从 @simon_he/vue-tui/vue 或 @simon_he/vue-tui/agent 导入。传 ascii=true 时会把该选项传给 renderer,要求 renderer 输出纯 ASCII。
使用内置 beautiful-mermaid bridge 前先安装:
pnpm add beautiful-mermaid然后从
@simon_he/vue-tui/mermaid或@simon_he/vue-tui/agent/mermaid导入TMermaidText/TMermaid。未安装
beautiful-mermaid时不要直接 import@simon_he/vue-tui/mermaid或@simon_he/vue-tui/agent/mermaid;请从@simon_he/vue-tui/vue/@simon_he/vue-tui/agent导入基础组件并显式传renderer。
Example
内置 beautiful-mermaid bridge:
<script setup lang="ts">
import { TMermaidText } from "@simon_he/vue-tui/agent/mermaid";
const diagram = `graph TD
Prompt --> Plan
Plan --> ToolCall
ToolCall --> Answer`;
</script>
<template>
<TMermaidText :x="0" :y="0" :w="72" :content="diagram" />
</template>基础组件 + 显式 renderer:
<script setup lang="ts">
import { TMermaidText } from "@simon_he/vue-tui/agent";
import { beautifulMermaidRenderer } from "@simon_he/vue-tui/agent/mermaid";
const diagram = `graph TD
Prompt --> Plan
Plan --> ToolCall
ToolCall --> Answer`;
</script>
<template>
<TMermaidText :x="0" :y="0" :w="72" :content="diagram" :renderer="beautifulMermaidRenderer" />
</template>流式 Mermaid source:
<TMermaidText
:x="0"
:y="0"
:w="72"
:content="diagram"
:streaming="message.streaming"
:final="message.final"
/>Props
x/y/w(number, required):渲染区域左上角与宽度h(number?):固定高度;不传时按渲染行数自适应content/code(string?):Mermaid source;同时传入时code优先final(boolean):Mermaid source 是否已经结束;streaming=true && final=false时只显示 source,不调用 renderer;非 streaming 或final=true时才尝试渲染并在成功时原子替换 sourceascii(boolean):使用纯 ASCII 而不是 Unicode box drawingoptions(TMermaidAsciiOptions?):传给 renderer 的 spacing/theme options;组件始终强制colorMode: "none"renderer(TMermaidRenderer?):自定义 renderer,适合测试或替换 Mermaid engineshouldRenderSource(TMermaidRenderEligibility?):可选 renderer eligibility guard;返回false时保持源码,不调用 renderer。primitive 不传时不额外限制;beautiful-mermaid wrapper 不传时默认使用isSimpleMermaidFlowchartSource,即只尝试简单 flowchart。isTransientError(TMermaidTransientErrorClassifier?):source-first 兼容遗留 prop;当前不会影响显示,渲染失败/超时时始终保持 source 显示markMermaidRenderErrorFatal(error):从@simon_he/vue-tui/vue或@simon_he/vue-tui/agent导入;source-first 渲染失败时仍保持源码显示streaming(boolean):streaming 更新时使用低优先级 frame task 合并重算loadingText/incompleteText/missingDependencyText/errorText(string):source-first 兼容遗留 prop;当前不会影响显示,失败/未完成时始终保持 source 显示showErrorDetails(boolean):source-first 兼容遗留 prop;当前渲染路径不会绘制错误详情
Streaming error policy
AI 输出 Mermaid fence 时经常会先产生不完整源码。streaming=true && final=false 下组件只显示源码,不调用 renderer。非 streaming 或 final=true 后:
- 如果当前源码通过 size guard 和 eligibility guard,且 renderer 成功,则显示渲染结果。
- 如果当前源码是复杂 Mermaid、超出 size guard、显式 eligibility 返回
false,或 renderer 失败/超时/返回空白,则保持源码显示。 - 缺少 renderer 时也保持源码显示,不显示 loading/error/incomplete 文案。
TVirtualMarkdown
Virtual Markdown renderer。它复用 TMarkdownText 的 markdown parsing / painting pipeline,并为长文档提供 viewport scrolling。
Markdown import:
@simon_he/vue-tui/markdown
TVirtualMarkdown默认保持文本可选中复制,即使它自身是 focusable 节点;如需列表式交互,可传selectable=false。Markdown link 会写入
Style.hrefmetadata。DOM renderer 默认不把Style.href渲染为原生<a>。启用links: true或links: { activation: 'native' }后,safe absolute 和 relative/hash/search href 会渲染为原生<a>,浏览器保留默认导航行为;onLinkClick返回false时阻止导航。启用links: { activation: 'event', onActivate }后,点击始终preventDefault(),由onActivate处理跳转、打开或路由。links: { activation: 'none' }不渲染原生 anchor,只保留文本。CLI/stdout renderer 只会为 safe absolute href 发出 OSC8 hyperlink。
TVirtualMarkdown当前仍是 viewport-level repaint,不是 row-local dirty diff;streaming append 也不会自动 follow tail,默认保持 absolutescrollTop/ absolute visual-row index 语义。
@simon_he/vue-tui/markdown公开createMarkdownBlockSource()、createTuiMarkdownParser()、buildMarkdownBlocks()、buildMarkdownVisualRows()与layoutMarkdownBlocks(),用于需要直接消费 block/visual row 或流式 transcript block source 的宿主渲染器。
TLogView
Append-only / streaming 日志视图:从 source / version 数据源读取可见窗口,不接收大数组,也不把日志内容放进 Vue deep reactivity。
Experimental API:当前从
@simon_he/vue-tui/experimental导出,暂不进入 root 入口。ansi=true支持 ANSI SGR styling,并可配合links=true解析 OSC8 hyperlinks;minimap 和 arbitrary variable-height rich rows 仍不是当前能力。完整组合示例见 TLogView Lab。
Props
x/y/w/h(number, required)source(TLogDataSource, required):提供lineCount()、getLine(index),可选提供getLineKey(index)、firstLineIndex()version(number, required):数据变化版本号;template 里Ref<number>会自动 unwrapscrollTop(number?):受控 visual-row scrollTop;省略时由组件内部维护defaultScrollTop(number?):非受控模式初始 visual-row scrollTop;省略时初始显示底部style(Style?)autoFocus(boolean)autoStickToBottom(boolean):在底部时 append 自动贴底,默认trueoverscan(number):wrap=true底部窗口预先测量的额外 visual rows,默认2wrap(boolean):长逻辑行按 cell width 拆成多个 visual rows,默认falsevisualIndexMode("estimated" | "exact"):wrapped visual row 总数的索引模式,默认"estimated"visualIndexOptions(TLogViewVisualIndexOptions?):measureBudgetMs默认4;maxMeasuredLines可限制 retained-window exact scan 范围ansi(boolean):解析每条 logical line 中的 ANSI SGR styling,默认falselinks(boolean):ansi=true时解析 OSC8 hyperlinks,默认falselinkStyle(Style?):link text 叠加样式;OSC8 links 默认保留日志原 ANSI 样式,仅叠加{ underline: true }或传入的linkStyle;linkify=true的自动链接会先叠加theme.components.TLink.style并遵循TLink.underline=falsekeyboardLinks(boolean):启用当前 visible OSC8 link 的键盘导航,默认falselinkFocusStyle(Style):focused visible link 的叠加样式,默认{ inverse: true }searchQuery(string):在当前 retained source window 内搜索 visible text,默认""searchOptions(TLogViewSearchOptions?):mode默认"text";caseSensitive/wholeWord默认false;maxMatches默认10_000;scanBudgetMs默认4;regexFlags可追加m/u/s等 flags;maxMatchesPerLine默认1_000highlightMatches(boolean):是否绘制 search matches,默认truematchStyle(Style):普通 match 高亮,默认{ inverse: true }currentMatchStyle(Style):当前 match 高亮,默认{ inverse: true, bold: true }rowScrollMode("off" | "unsafe-full-row"):full-row append 的 opt-in unsafe row-scroll 优化,默认"off"
Data source
import { createAppendOnlyLogStore } from "@simon_he/vue-tui/experimental";
const log = createAppendOnlyLogStore({ maxLines: 10_000 });
log.appendChunk("hello");
log.appendChunk(" world\nnext line");<TLogView :source="log.source" :version="log.version" :x="0" :y="0" :w="80" :h="20" />createAppendOnlyLogStore({ maxLines }) 使用普通 store 和单独的 version ref。不要把日志行做成 reactive array,也不要每次 append 都重建全文字符串。maxLines 省略时不限制;设置后只保留最近的 logical lines,completed lines 和当前 mutable tail 都计入保留窗口。
createAppendOnlyLogStore() 保存 completed lines 和一个 mutable tail。appendChunk() 会追加到 tail,并按 \n 拆出 completed lines;appendLine() 如果存在 tail,会先完成 tail + line,否则追加一条 completed line;replaceTail() 只替换 mutable tail,不会修改最后一条 completed line。
启用 retention 时,log.source.firstLineIndex() 返回 source index 0 对应的绝对 logical line number。clear() 会把 retained window 和 firstLineIndex() 一起重置为 0。
appendLine() / appendLines() 期望调用方传入单行文本;需要处理包含 newline 的 streaming 输入时,请使用 appendChunk()。
TLogView 会按行缓存 fixed one-line 的最终 render string。wrap=true 时还会按 getLineKey(index) + width 缓存每条逻辑行拆出的 visual rows。ansi=true 时会单独缓存 ANSI parsed segments、ANSI wrapped visual rows 和 clipped styled row 结果。createAppendOnlyLogStore() 会为 completed lines 提供稳定 key,并在 tail 文本变化时更换 tail key,让 streaming / append-only 场景复用已完成历史行的 clipped/padded 输出和 wrap 结果。
自定义 source 如果能提供稳定身份,建议实现:
type TLogDataSource = {
lineCount(): number;
getLine(index: number): string;
getLineKey?: (index: number) => string | number;
firstLineIndex?: () => number;
};getLineKey(index) 应在同一行文本不变时保持稳定;mutable tail 或可见历史行文本变化时必须改变。source identity 也应尽量稳定;如果替换整个 source 对象,TLogView 会清空实例内 render cache。
未提供 getLineKey 时,TLogView 会退回到 version + index,确保 version 变化后不会出现 stale text,但跨 version 的缓存复用会受限。
TLogView 仍按 append-only / tail-only mutation 优化。getLineKey(index) 用于缓存正确性和 append/tail 场景;它不是任意历史行 diff 机制。自定义 source 如果会修改任意可见历史行,应替换 source identity,或等待后续 explicit viewport refresh API。
wrap=false 是默认行为,超出宽度的行会被 clip。wrap=true 时,一个 logical source line 可以渲染成多个 visual rows,scrollTop 也按 visual row 计数。ansi=false 是默认行为,日志行按纯文本 fast path 渲染。ansi=true 时,source.getLine(index) 可以包含 ANSI SGR escape sequences;TLogView 会解析 fg/bg/bold/dim/italic/underline/inverse 等 style,并在 fixed clip 和 wrap=true visual rows 中保留样式。ANSI reset 会回到 TLogView base style(style prop 或 terminal default style)。
links=true 只在 ansi=true 时生效。TLogView 会解析 OSC8 opener/closer,忽略 params,把 safe visible link text 渲染为带 Style.href 的 cells。OSC8 links 不读取 theme.components.TLink.style,默认只在日志原 ANSI 样式上叠加 { underline: true },传入 linkStyle 时改为叠加该样式;unsafe href 会按普通文本渲染,不进入 visible link model。BEL 和 ST terminator 都支持。组件不会自动打开链接、不会解析 Markdown link,也不会提供 hover tooltip;点击 visible link cell 时只 emit linkClick,由应用层决定如何处理。
linkify=true 只在 ansi=false 时生效,用同 TLinkifyText 一致的 URL 检测规则把纯文本里的 safe URL 渲染为 Style.href cells。linkify 生成的普通文本链接会使用 theme.components.TLink.style,再叠加 linkStyle。需要调整协议范围时可传对象::linkify="{ protocols: ['https'], allowRelative: true }"。如果日志已经带 ANSI/OSC8,应使用 ansi + links,不要叠加 linkify。
keyboardLinks=false 是默认行为,这样 Tab / Enter 不会默认抢占宿主应用自己的焦点和提交逻辑。启用 keyboardLinks 后,TLogView 在获得焦点时会只针对当前 visible links处理键盘:Tab / Shift+Tab 在当前 viewport 内可见的 OSC8 link segments 间循环 focus,Enter emit linkActivate,Escape 清除当前 focused link。getVisibleLinks() 返回的也是当前 visible / clipped link segments,不会为 retained source window 建立全局 link index。
searchQuery 只搜索 visible text。searchOptions.mode="text" 时使用 plain substring matching;mode="regex" 时把 searchQuery 作为 JavaScript RegExp pattern string 编译。ansi=true 时,ANSI escape sequences 不参与搜索,也不会污染 match offset;match highlight 会叠加在 ANSI style 上。match 坐标使用 terminal cell offset,因此宽字符会按 cell width 定位。wrap=true 时,findNext() / findPrevious() / selectSearchMatch() 都会滚动到 match 所在 visual row。搜索范围始终是当前 retained source window;retention trim、append、tail mutation、source 或 version 变化后会基于当前窗口重新扫描。
mode="regex" 时,内部始终追加 g flag 来扫描所有 matches;caseSensitive=false 会追加 i;regexFlags 可以传 m / u / s 等额外 flags。传入的 g 会被忽略,因为组件内部需要控制 lastIndex;y 也会被忽略。invalid regex 不会抛出到组件外:search state / search payload 会进入 status: "error",同时暴露 { kind: "invalid-regex", query, flags, message },并清空 matches、markers 和 highlights。
getSearchMarkers() 会把当前 search matches 投影成 scrollbar-friendly marker 数据:visualRow 仍然是 retained window 内的 visual-row index,absoluteLineIndex 保留原始 logical line 编号,estimated 用来区分 lazy visual index 和 exact visual index,current 表示当前 match。这个方法只基于当前 search/visual index 状态计算 markers,不会为了 marker 数据强制做一次全量 exact measurement。
当外部 UI(例如 scrollbar marker、后续的 search result panel)需要把某个 match 设为 current match 时,应该调用 selectSearchMatch(matchIndex)。scrollToVisualRow(marker.visualRow) 只会移动 viewport,不会更新 currentMatchIndex,也不会切换 current marker / currentMatchStyle 或按新的 current match 继续 findNext() / findPrevious()。getSearchMatch() 和 getSearchResults() 只返回 lightweight match 引用,不会触发滚动、事件或 preview 生成;getSearchResults() 当前也不会生成 line preview/snippet。
searchOptions.wholeWord 只在 mode="text" 下生效,并使用 ASCII word boundary:[A-Za-z0-9_]。例如 error-1 中的 error 会被视为 whole-word match,而 _error 不会。regex mode 下如果需要 word boundary,请显式写 \b 或 lookaround。
clearSearch() 会 emit update:searchQuery,并等待父组件把 searchQuery 回写为空后清除 matches;如果父组件不回写,当前 search state 和 highlight 不会提前改变。
搜索扫描通过 scheduler frame task 分帧执行,默认每帧最多使用约 4ms,不会在 searchQuery 变化时同步读取全部 retained lines。maxMatches 默认限制为 10_000。regex mode 额外提供 maxMatchesPerLine guard,默认每行最多记录 1_000 个 match。注意:分帧只能切开多行扫描,无法中断单条超长日志上的高开销 regex 求值。
<TLogView
:source="log.source"
:version="log.version"
search-query="ERROR\\s+\\d+"
:search-options="{ mode: 'regex', caseSensitive: false }"
/>当前不支持 fuzzy/semantic search、cursor movement、clear screen、alternate buffer、syntax highlight、markdown/rich text 或 arbitrary variable-height row model。
Scroll behavior
scrollTop始终是 visual-row 语义:wrap=false时 visual row 等于 logical line;wrap=true时一条 logical line 可能占多个 visual rows。scrollTop相对于当前 retained source window;启用 retention 后,旧 head lines 被 trim 时firstLineIndex()会增加。- 非受控模式:省略
scrollTop,TLogView内部维护滚动位置;defaultScrollTop只在初始 mount 使用一次,省略时初始显示底部。 - 受控模式:传入
scrollTop并监听update:scrollTop。TLogView会 emit 期望的 nextscrollTop,但不会在父组件回写 prop 前改变渲染出来的 rows。 - 受控模式下
scrollTop是 source of truth;autoStickToBottom只会在贴底 append 时 emit 新底部位置,不会绕过父组件直接改变视图。 - retention trim 发生时,非受控模式会尽量调整
scrollTop保持当前可见内容锚定;受控模式只 emit 调整后的update:scrollTop,等待父组件回写。 - 建议不要在同一个
TLogView生命周期中切换 controlled/uncontrolled 模式;需要切换时重新挂载组件或显式管理scrollTop。 appendLine/appendChunk/replaceTail通过scheduler.queueFrameTask()合并为 stream frame。TLogView每次 paint 只读取当前 visible rows;命中 line-level render cache 的行不会再次调用source.getLine()。wrap=true初始底部和 append-only streaming 路径只测量 bottom/visible window 加 overscan,不会为了计算 wrap 结果全量读取大日志。visualIndexMode="estimated"是默认行为:wrap=true时只对 bottom/visible path lazy-measure visual rows。bottom stickiness、append 和 visible-window rendering 是准确路径;全量 scrollbar / jump-to-percent 所需的 total visual rows 在大日志上是估计值,除非所有行都被测量。visualIndexMode="exact"会在 scheduler frame task 中分帧构建 retained-window exact visual index,不会同步全量 wrap。scrollpayload 和getScrollMetrics()会额外暴露visualIndexStatus/visualRowCount/measuredLineCount等 metrics。measureVisualIndex()可以在默认 estimated 模式下手动触发一次后台 exact measurement;measurement 过程中visualIndexStatus会从estimated进入measuring,完成后变为exact。scrollToLine()会测量目标 logical line,并定位到该行的第一个 visual row;wrap=true下目标行之前尚未测量的 wrapped rows 仍按 lazy visual index 估计。- 初始 mount 默认显示当前底部;
autoStickToBottom=false只影响后续 append 是否继续贴底,不改变初始定位。 - 用户在底部时 append 会贴底;用户滚离底部后 append 不改变当前
scrollTop,也不会 repaint 当前 viewport。 rowScrollMode="unsafe-full-row"只有在组件占满整行、未被裁剪、renderer 支持scrollOperations时才会用 exposed-row repaint;其它情况回退到 viewport repaint。
<script setup lang="ts">
import { ref } from "vue";
import type { TLogViewHandle } from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const query = ref("ERROR");
const metrics = ref(logView.value?.getScrollMetrics());
function jumpBottom() {
logView.value?.scrollToBottom();
}
function nextError() {
logView.value?.findNext();
}
function nextLink() {
logView.value?.focusNextLink();
}
function measureRows() {
logView.value?.measureVisualIndex();
}
</script>
<TLogView
ref="logView"
:source="log.source"
:version="log.version"
:search-query="query"
wrap
visual-index-mode="exact"
:visual-index-options="{ measureBudgetMs: 4 }"
:ansi="true"
:links="true"
keyboard-links
@visualIndex="metrics = logView?.getScrollMetrics?.()"
@linkClick="(payload) => console.log(payload.href)"
@linkActivate="(payload) => console.log(payload.link.href)"
:x="0"
:y="0"
:w="80"
:h="20"
/>应用层可以用 getVisibleLinks() / focusVisibleLink() / focusNextLink() / focusPreviousLink() / clearLinkFocus() / activateFocusedLink() 做自己的命令面板或快捷键桥接。linkClick 是 pointer-only 事件;linkActivate 只表示 keyboard/programmatic activation,组件本身不会打开浏览器。
TLogScrollbar
TLogScrollbar 是一个 terminal-rendered 的 experimental companion 组件,消费 TLogViewScrollMetrics 渲染 1-cell 宽滚动条。它不持有滚动状态,也不会直接调用 TLogView 内部逻辑;父组件负责把 scrollTo / scrollBy / markerClick 接到 TLogViewHandle 或应用层状态上。
metrics可以直接来自logView.value?.getScrollMetrics(),也可以由@scroll/@visualIndex事件在外部维护visualIndexStatus="estimated" | "measuring" | "exact"会分别用不同 thumb 状态渲染,方便区分估算值、后台测量中和精确 visual-row indexmarkers可以直接来自logView.value?.getSearchMarkers();scrollbar 只根据 marker 的visualRow/current/estimated渲染,不依赖TLogView私有状态markerClick只把当前可见的 marker row 回传给父组件;如果 marker 与 thumb 落在同一 row,thumb 仍然保持视觉和交互优先级- 点击 thumb 当前所在 row 仍然走 track click 语义:emit 目标 visual-row
scrollTo,不是 drag handle - wheel 会 emit 简单的
scrollBy(+/-1)delegation showArrows=true时首尾两行会渲染▲/▼,点击后按一个 viewport 高度翻动;当h < 2时 arrows 会自动禁用metrics建议总是替换 fresh object,而不是原地 mutate 旧对象,方便 renderer 和父组件同步
<script setup lang="ts">
import { onMounted, ref } from "vue";
import {
TLogScrollbar,
TLogView,
type TLogScrollbarMarker,
type TLogViewHandle,
type TLogViewSearchMarker,
type TLogViewScrollMetrics,
} from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const metrics = ref<TLogViewScrollMetrics | null>(null);
const markers = ref<readonly TLogScrollbarMarker[]>([]);
const query = ref("ERROR");
function refreshMetrics() {
metrics.value = logView.value?.getScrollMetrics() ?? null;
markers.value =
logView.value?.getSearchMarkers().map((marker) => ({
id: marker.matchIndex,
visualRow: marker.visualRow,
current: marker.current,
estimated: marker.estimated,
payload: marker,
})) ?? [];
}
function scrollTo(top: number) {
logView.value?.scrollToVisualRow(top);
refreshMetrics();
}
function scrollBy(delta: number) {
logView.value?.scrollBy(delta);
refreshMetrics();
}
function onMarkerClick(payload: {
marker: TLogScrollbarMarker & { payload?: TLogViewSearchMarker };
}) {
const marker = payload.marker.payload;
if (!marker) return;
logView.value?.selectSearchMatch(marker.matchIndex, {
align: "center",
});
refreshMetrics();
}
onMounted(refreshMetrics);
</script>
<TLogView
ref="logView"
:x="0"
:y="0"
:w="79"
:h="20"
:source="log.source"
:version="log.version"
wrap
visual-index-mode="exact"
:search-query="query"
@scroll="refreshMetrics"
@visualIndex="refreshMetrics"
@searchMarkers="refreshMetrics"
/>
<TLogScrollbar
:x="79"
:y="0"
:h="20"
:metrics="metrics"
:markers="markers"
@scrollTo="scrollTo"
@scrollBy="scrollBy"
@markerClick="onMarkerClick"
/>TLogMinimap
TLogMinimap 是一个 experimental compact overview companion 组件,用来把 retained visual rows 压缩成 1 列或多列 overview。它只消费父组件传入的 metrics、markers 和可选 density buckets,不会直接读取 TLogView 或 log source,也不会尝试渲染真实文本缩略图。
metrics通常来自logView.value?.getScrollMetrics()markers通常来自logView.value?.getSearchMarkers();minimap 只根据visualRow/current/estimated渲染density是应用层聚合结果,例如日志密度、错误密度或搜索结果密度;TLogView不会自动生成density.endVisualRow按 inclusive end 处理- 点击空白 row 会 emit
{ visualRow, cellX, cellY }的scrollTo - 点击 marker row 任意列都会 emit
markerClick,且不会额外触发scrollTo scrollTo.visualRow建议直接交给TLogView.scrollToVisualRow()做 clamp- 多列 minimap 中 density char 可以和 viewport style 叠加
- 第一版只做 overview,不做 hover tooltip、拖拽 viewport 或 content minimap
<script setup lang="ts">
import { onMounted, ref } from "vue";
import {
TLogMinimap,
TLogView,
type TLogMinimapDensityBucket,
type TLogMinimapMarker,
type TLogViewHandle,
type TLogViewScrollMetrics,
type TLogViewSearchMarker,
} from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const metrics = ref<TLogViewScrollMetrics | null>(null);
const markers = ref<readonly TLogMinimapMarker[]>([]);
const density = ref<readonly TLogMinimapDensityBucket[]>([]);
const query = ref("ERROR");
function refreshOverview() {
metrics.value = logView.value?.getScrollMetrics() ?? null;
markers.value =
logView.value?.getSearchMarkers().map((marker) => ({
id: marker.matchIndex,
visualRow: marker.visualRow,
current: marker.current,
estimated: marker.estimated,
payload: marker,
})) ?? [];
density.value = [
{ startVisualRow: 0, endVisualRow: 40, value: 0.15 },
{ startVisualRow: 41, endVisualRow: 120, value: 0.55 },
{ startVisualRow: 121, endVisualRow: 200, value: 0.9 },
];
}
function onMarkerClick(payload: {
marker: TLogMinimapMarker & { payload?: TLogViewSearchMarker };
}) {
const marker = payload.marker.payload;
if (!marker) return;
logView.value?.selectSearchMatch(marker.matchIndex, {
align: "center",
});
refreshOverview();
}
onMounted(refreshOverview);
</script>
<TLogView
ref="logView"
:x="0"
:y="0"
:w="78"
:h="20"
:source="log.source"
:version="log.version"
wrap
visual-index-mode="exact"
:search-query="query"
@scroll="refreshOverview"
@visualIndex="refreshOverview"
@searchMarkers="refreshOverview"
@searchMatch="refreshOverview"
/>
<TLogMinimap
:x="78"
:y="0"
:w="2"
:h="20"
:metrics="metrics"
:markers="markers"
:density="density"
@scrollTo="({ visualRow }) => logView?.scrollToVisualRow(visualRow)"
@markerClick="onMarkerClick"
/>TLogSearchBar
TLogSearchBar 是一个 experimental controlled search input companion。它只负责搜索 query / mode / toggle / navigation 的输入与展示,不会直接读取 TLogView,也不会自己执行搜索;父组件负责把 query/options 传给 TLogView,再把 TLogViewHandle.getSearchState() 回填给 search bar。
- query 是受控值,组件内部只维护 cursor / focus / scroll offset
Enteremitnext,Shift+Enteremitprevious,Escemitclear- click
[T]/[R]、[Aa]、[W]只 emit update events,不直接触发搜索 mode="regex"时wholeWordtoggle 会渲染为 disabled,并且 click 不会 emitupdate:wholeWordstate.status可以驱动Scanning…、current/matchCount、Invalid regex等展示
<script setup lang="ts">
import { computed, ref } from "vue";
import {
TLogSearchBar,
TLogView,
type TLogSearchBarState,
type TLogViewHandle,
} from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const query = ref("");
const mode = ref<"text" | "regex">("text");
const caseSensitive = ref(false);
const wholeWord = ref(false);
const searchState = ref({
status: "idle",
matchCount: 0,
currentMatchIndex: -1,
error: null,
});
function refreshSearchUi() {
const next = logView.value?.getSearchState();
searchState.value = {
status: next?.status ?? "idle",
matchCount: next?.matchCount ?? 0,
currentMatchIndex: next?.currentMatchIndex ?? -1,
error: next?.error ?? null,
};
}
const barState = computed<TLogSearchBarState>(() => ({
query: query.value,
mode: mode.value,
caseSensitive: caseSensitive.value,
wholeWord: wholeWord.value,
status: searchState.value.status,
matchCount: searchState.value.matchCount,
currentMatchIndex: searchState.value.currentMatchIndex,
error: searchState.value.error,
}));
</script>
<TLogSearchBar
:x="0"
:y="0"
:w="80"
:state="barState"
@update:query="query = $event"
@update:mode="mode = $event"
@update:caseSensitive="caseSensitive = $event"
@update:wholeWord="wholeWord = $event"
@previous="logView?.findPrevious()"
@next="logView?.findNext()"
@clear="query = ''"
/>
<TLogView
ref="logView"
:x="0"
:y="1"
:w="80"
:h="20"
:source="log.source"
:version="log.version"
:search-query="query"
:search-options="{
mode,
caseSensitive,
wholeWord,
}"
@search="refreshSearchUi"
@searchMatch="refreshSearchUi"
/>TLogSearchResults
TLogSearchResults 是一个 experimental search result panel,用来渲染当前页搜索结果预览。它只消费外部准备好的 result items,不持有 search state,也不会直接读取 TLogView 或 log source。
- 分页状态建议交给
useTLogSearchResultsPage TLogSearchResults仍然只负责结果列表渲染和 row-level 交互selectpayload 里的matchIndex仍然是 global match index,父组件或 composable 负责调用selectSearchMatch(matchIndex)includePreview默认是false,避免每次 getter 都去读取结果行内容- preview 基于 visible text 生成;
ansi=true时不会把 ANSI escape sequences 带进结果面板 - preview 和高亮 offset 都按 cell 计算,因此宽字符场景可以保持匹配位置正确
- 组件只负责渲染和交互:
ArrowUp/ArrowDown/Home/End更新 active row,Enter和 click emitselect activeIndex是 external sync hint:click / keyboard 会先更新内部 active row,再 emitactiveChange;父组件可在下一拍重新同步results应该是当前 page/window,通常长度不超过组件高度;组件不会自己 virtualize 整个结果集- 不要每一帧都对全部 10k matches 调
includePreview: true;应当只给当前页或当前窗口取 preview
<script setup lang="ts">
import { ref } from "vue";
import {
TLogSearchPager,
TLogSearchResults,
TLogView,
useTLogSearchResultsPage,
type TLogSearchResultsSelectPayload,
type TLogViewHandle,
} from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const query = ref("ERROR");
const { state, refresh, previousPage, nextPage, selectResult } = useTLogSearchResultsPage(logView, {
pageSize: 20,
includePreview: true,
previewWidth: 60,
});
function onSelect(payload: TLogSearchResultsSelectPayload) {
selectResult(payload.matchIndex);
}
</script>
<TLogView
ref="logView"
:x="0"
:y="0"
:w="60"
:h="20"
:source="log.source"
:version="log.version"
:search-query="query"
@search="refresh"
@searchMatch="refresh"
/>
<TLogSearchResults
:x="61"
:y="0"
:w="19"
:h="19"
:results="state.results"
:active-index="state.activeIndex"
@select="onSelect"
/>
<TLogSearchPager
:x="61"
:y="19"
:w="19"
:state="state"
@previousPage="previousPage"
@nextPage="nextPage"
/>TLogSearchPager
TLogSearchPager 是一个 experimental presentational pager companion,用来渲染搜索结果页码、match count 和上一页/下一页控制。它只消费外部传入的分页状态,不会直接读取 TLogView,也不会自己调用 search API。
idle显示No searchscanning显示Scanning…和当前已发现 match countdone + matchCount=0显示No matcheserror显示Invalid regexdone显示单行 pager,例如◀ 2/13 245 matches ▶- click
◀/▶和ArrowLeft/ArrowRight/PageUp/PageDown都只 emit,父组件负责接到 composable 或 handle previousPage/nextPage和pageChange是两套等价事件;通常只处理其中一套,避免重复翻页
useTLogSearchResultsPage 负责从 TLogViewHandle 拉取当前页结果、同步 page-local activeIndex、clamp 页码,并在 selectResult(matchIndex) 时调用 selectSearchMatch(matchIndex) 后刷新当前页状态。推荐把它和 TLogSearchResults / TLogSearchPager 一起使用,而不是在父组件里重复手写 offset、pageSize、currentMatchIndex 同步逻辑。它的 pageSize / includePreview / previewWidth / contextCells 都是 setup-time configuration;如果这些值需要动态变化,请重新创建 controller/composable。
Search UX suite wiring
如果你希望把 TLogSearchBar、TLogSearchResults、TLogSearchPager、TLogScrollbar 和 TLogMinimap 一起接成完整搜索体验,推荐直接使用 useTLogSearchController。它会集中管理:
query/mode/caseSensitive/wholeWordsearchBarStateresultsPagemarkersmetricssearchHistory/savedSearches
useTLogSearchController 的 pageSize / includePreview / previewWidth / contextCells / maxHistory / initialSavedSearches 都按 setup-time configuration 处理;如果这些值要动态变化,建议重建 controller。
<script setup lang="ts">
import { ref } from "vue";
import {
TLogMinimap,
TLogScrollbar,
TLogSearchBar,
TLogSearchPager,
TLogSearchResults,
TLogView,
useTLogSearchController,
type TLogViewHandle,
} from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const search = useTLogSearchController(logView, {
pageSize: 20,
includePreview: true,
previewWidth: 64,
});
const {
query,
mode,
caseSensitive,
wholeWord,
regexFlags,
searchBarState,
resultsPage,
markers,
metrics,
refresh,
previousMatch,
nextMatch,
clearSearch,
selectMatch,
} = search;
const { state: resultsPageState } = resultsPage;
function refreshSuite() {
refresh();
}
</script>
<TLogSearchBar
:x="0"
:y="0"
:w="80"
:state="searchBarState"
@update:query="search.updateQuery"
@update:mode="search.updateMode"
@update:caseSensitive="search.updateCaseSensitive"
@update:wholeWord="search.updateWholeWord"
@previous="previousMatch"
@next="nextMatch"
@clear="clearSearch"
/>
<TLogView
ref="logView"
:x="0"
:y="1"
:w="60"
:h="20"
:source="log.source"
:version="log.version"
:search-query="query"
:search-options="{
mode,
caseSensitive,
wholeWord,
regexFlags,
}"
@scroll="refreshSuite"
@visualIndex="refreshSuite"
@search="refreshSuite"
@searchMatch="refreshSuite"
@searchMarkers="refreshSuite"
/>
<TLogSearchResults
:x="61"
:y="1"
:w="19"
:h="17"
:results="resultsPageState.results"
:active-index="resultsPageState.activeIndex"
@select="({ matchIndex }) => selectMatch(matchIndex)"
/>
<TLogSearchPager
:x="61"
:y="18"
:w="19"
:state="resultsPageState"
@previousPage="resultsPage.previousPage"
@nextPage="resultsPage.nextPage"
/>
<TLogScrollbar
:x="80"
:y="1"
:h="20"
:metrics="metrics"
:markers="markers.map((marker) => ({ visualRow: marker.visualRow, current: marker.current }))"
/>
<TLogMinimap
:x="81"
:y="1"
:w="2"
:h="20"
:metrics="metrics"
:markers="markers.map((marker) => ({ visualRow: marker.visualRow, current: marker.current }))"
/>TLogLinksPanel
TLogLinksPanel 是一个 experimental visible-link panel,只渲染 当前 viewport 内可见的 OSC8 links。它不扫描 retained window,也不直接读取 TLogView;父组件负责把 getVisibleLinks() 或 useTLogLinkController.visibleLinks 回填给它。
- 每行展示
absoluteLineIndex + text + href activeIndex是 external sync hint;panel 会先更新内部 active row,再通过activeChange把当前行交回父组件current用来标记TLogView当前 focused visible linkEnter只 emitactivate,不会自动打开浏览器links应该是当前 viewport/window 内的 visible links;组件不会自己维护完整 retained-window 历史
Link UX suite wiring
如果你希望把 TLogView 的 visible OSC8 link 导航、panel 展示和应用层 action 统一起来,推荐直接使用 useTLogLinkController。它会集中管理:
visibleLinksactiveIndexfocusVisibleLink/focusNextLink/focusPreviousLinkactivateVisibleLink/activateFocusedLinkhandleLinkClick/handleLinkActivate
useTLogLinkController 不会建立 global link index,也不会自动打开链接。onAction 只把 { href, text, source, absoluteLineIndex, index, startCell, endCell } 交回应用层,由应用决定 open/copy/preview。refresh() 需要由父组件在 scroll / linkFocus / linkClick / linkActivate 以及 source/version 变化后显式调用,因为 visible links 会跟着 viewport 变化。它的 options 也是 setup-time configuration。
<script setup lang="ts">
import { ref } from "vue";
import {
TLogLinksPanel,
TLogView,
useTLogLinkController,
type TLogLinksPanelActiveChangePayload,
type TLogViewHandle,
} from "@simon_he/vue-tui/experimental";
const logView = ref<TLogViewHandle | null>(null);
const linkController = useTLogLinkController(logView, {
onAction(action) {
console.log("Link action:", action.href, action.source);
},
});
const {
visibleLinks,
activeIndex,
refresh,
focusVisibleLink,
clearFocus,
activateVisibleLink,
handleLinkClick,
handleLinkActivate,
} = linkController;
function refreshLinks() {
refresh();
}
function onPanelActiveChange(payload: TLogLinksPanelActiveChangePayload) {
if (payload.item) focusVisibleLink(payload.item.visibleIndex);
else clearFocus();
}
</script>
<TLogView
ref="logView"
:x="0"
:y="0"
:w="60"
:h="20"
:source="log.source"
:version="log.version"
ansi
links
keyboard-links
@scroll="refreshLinks"
@linkFocus="refreshLinks"
@linkClick="handleLinkClick"
@linkActivate="handleLinkActivate"
/>
<TLogLinksPanel
:x="61"
:y="0"
:w="19"
:h="20"
:links="visibleLinks"
:active-index="activeIndex"
@select="({ visibleIndex }) => focusVisibleLink(visibleIndex)"
@activeChange="onPanelActiveChange"
@activate="({ visibleIndex }) => activateVisibleLink(visibleIndex)"
/>启用 keyboardLinks=true 后,Tab / Shift+Tab / Enter 仍然只作用于当前 visible links;TLogLinksPanel 也同样只操作当前 visible 列表,不会越过 viewport 建立 retained-window 级别的历史索引。
Events
scroll:{ scrollTop, atBottom, lineCount, estimatedVisualRowCount, visualRowCount, measuredVisualRowCount, measuredLineCount, visualIndexStatus, firstLineIndex }update:scrollTop:scrollTop(visual row)update:searchQuery:searchQuerysearch:{ query, status, matchCount, error }searchMatch:{ match, currentMatchIndex, matchCount }searchMarkers:{ markers, visualIndexStatus, matchCount, currentMatchIndex }linkClick:{ href, text, absoluteLineIndex, index, startCell, endCell, cellX, cellY }linkFocus:{ link, focusedLinkIndex }linkActivate:{ link, source }focus/blur/keydown
TSelect
选择器:单选/多选两种模式,支持点击、键盘 ↑/↓/Enter/Esc,多选支持 Space 切换。
Props(核心)
x/y/w/h(number, required)options((string | { label, detail? })[], default [])modelValuefollowsvalueMode: index values by default, option values forvalue, option objects foroptionmultiple(boolean)valueMode('index'|'value'|'option')optionProvider(query, { signal })loads async options for the current query; stale aborted requests do not emitloadError- When using
optionProvider, prefervalueMode="value"with stable optionvaluefields.valueMode="option"compares selected options by object identity, so async providers that return fresh objects may not preserve selection across reloads. - Provider loading, or
loading=true, rendersloadingText; provider rejection renderserrorTextand emitsloadError multipleEmit('label'|'value'|'index'|'both');labelemits labels,valueemits option values,indexemits indices, andbothemits{ indices, labels, values }valueModecontrols themodelValueshape.multipleEmitcontrols thechange/confirmpayload shape separately. For object options withvalue, setmultipleEmit="value"ifchange/confirmshould emit option values instead of labels.searchableemitsupdate:queryfrom typed characters; it does not filter localoptionsby itselfstyle/highlightStyle(Style?)autoFocus(boolean)closeOnBlur(boolean)
Events
change: single-select emits the selected label ornull;valueModeonly affectsv-modelconfirm: multi-select emits the payload selected bymultipleEmitloadError:{ query, error }when the active async provider request rejectsclose/focus/blur/keydown
TPathPicker
路径输入 + 自动补全(Tab completion),用于 CLI 场景选择文件路径。
Props
x/y/w/h(number, required)workspace(string, required): 工作区绝对路径(用于解析与补全)mode('any'|'file'|'directory')modelValue(string)+update:modelValueplaceholder(string?)showHidden(boolean)maxSuggestions(number)provider(PathPickerProvider?): 局部覆盖路径 provider;也可通过TerminalProvider.pathPickerProvider或createTerminalApp({ pathPickerProvider })统一注入style(Style?)autoFocus(boolean)
Events
select:absPath(string)invalid:{ reason, absPath }(reason:not_found|not_file|not_directory|provider_missing)keydown/focus/blur
Keyboard
Tab: 应用当前补全项↑/↓/Ctrl+P/Ctrl+N: 选择补全项Enter: 提交(若当前输入/选中项可用则触发select;file 模式下目录会进入该目录)
跨宿主注意:
TPathPicker不再在组件本体里兜底 Node 文件系统实现。更推荐由宿主显式传入provider,或通过TerminalProvider.pathPickerProvider/createTerminalApp({ pathPickerProvider })注入;CLI 宿主可直接复用createNodePathPickerProvider()。细节以实现与回归测试为准:
src/vue/components/TPathPicker.ts。
TDialog
对话框/模态层:常用于 confirm/cancel、内容滚动、按钮组与焦点管理。
Props(核心)
modelValue(boolean)+update:modelValuew/h(number, required)title(string)/padding(number)zIndex(number)/style(Style?)placement:center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-rightoffsetX/offsetY(number)backdrop(boolean):是否有遮罩层closeOnBackdrop/closeOnEsc/closeOnBlur(boolean)teleport(boolean):通过 runtime portal 挂载到根层(避免被父 clip)buttons(DialogButton[]): 底部按钮(支持kind/default/value/id)closeOnConfirm(boolean)
Events
close/focus/blur/keydownconfirm:{ label, value?, id?, kind?, default?, index }
TTransition
终端版的过渡封装:show 改变时执行 enter/leave 钩子与时间插值,并通过 VisibilityContext 控制子树可见性。
Props
show(boolean, required)duration(number)beforeEnter/enter/afterEnter(hook?)beforeLeave/leave/afterLeave(hook?)
Slots
default:({ phase, progress }) => VNode
TDebugOverlay
调试覆盖层:可绘制 focus rect / 所有节点 rect,并显示 trace/dirtyRows 等信息。
Props
mode('focus'|'all')panel(boolean)maxRects(number)zIndex(number)
TMultilineModal
用于展示多行文本的简单模态层(带遮罩与边框),常见于“查看详细内容/日志”。
Props
visible(boolean, required)content(string, required)title(string)(默认Multiline Text)style(Style?)zIndex(number)(默认1000)
Events
close: 点击遮罩或按Esc时触发
Notes
- 尺寸:默认按终端
80%宽、70%高居中渲染 - 当前实现不支持滚动(内容会按可视区域截断)
TRouterView
终端版 RouterView:配合 createTerminalRouter() 使用,用于根据当前 route 渲染匹配的页面组件。
Props
routes(TerminalRouteRecord[], required): 路由表forceRemount(boolean): route 变化时是否强制 remount(默认true)
TRenderLayer
渲染层级容器:为子树创建一个新的 render stack,从而把整棵子树整体抬升/降低 zIndex(不影响事件 zIndex)。
Props
zIndex(number):相对父 stack 的偏移(默认0)
Slots
default
TRenderPlane
plane 分层容器:为子树切换到指定 render plane,并把 terminal、scheduler.invalidate()、runtime.mount() 自动绑定到该 plane。
Props
plane('default'|'transcript'|'chrome'|'overlay')(默认default)
Slots
default
Notes
TRenderPlane不改变布局矩形,也不创建新的 render stack- 它解决的是“这棵子树属于哪个 plane”,不是“这棵子树的 zIndex 是多少”
- 典型用法是把正文放进
transcript,把 footer/loading 放进chrome,把 dialog 放进overlay TRenderPlane.plane在 mount 后按 immutable 处理;如果需要把子树移动到另一个 plane,请按 plane 给TRenderPlane加key,例如<TRenderPlane :key="activePlane" :plane="activePlane">- 不要依赖动态修改
planeprop 来迁移已 mount subtree;tab switching、dialog migration、animation plane 迁移都应 key remount - frame tasks and scheduler invalidates default to the mounted plane, but an explicit
planefield still escapes that default. Passingctx.invalidate({ plane: undefined })escapes the mounted plane and is treated by the root scheduler as an all-plane invalidate. - frame task / mailbox ids remain scheduler-global. Include both plane and instance identity in custom ids to avoid cross-plane coalescing, for example
MyNode:${plane}:${uid}:stream.