Skip to content

Components 使用文档

本文档覆盖 @simon_he/vue-tui 当前内置的 Vue 组件,用于统一「渲染/参数/事件」的契约,便于实现一致的验收与测试。

坐标/尺寸单位:所有 x/y/w/h 均以「cell(字符格)」为单位,而不是像素。

完整的 Props/Events 列表请以自动生成文件为准:docs/generated/components-api.md(运行 pnpm run docs:gen 生成)。

导入入口

API maturityImport组件
Public@simon_he/vue-tuiTerminalProvider TBox TCommandPalette TDataTable TDialog TInput TLink TLinkifyText TList TSelect TTable TText TTree TView form helpers 和 TBadge/TTag/TDivider/TCode
Advanced@simon_he/vue-tui/vueTAnchor 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/markdownTMarkdownText TVirtualMarkdown
Public@simon_he/vue-tui/mermaidTMermaid TMermaidImage TMermaidText TBeautifulMermaidText beautifulMermaidRenderer createBeautifulMermaidRenderer
Experimental@simon_he/vue-tui/experimentalTCandlestickChart TContributionGraph TLineChart TPieChart TVideo TVirtualList TTranscriptView TLogView TLogSearchBar TLogSearchResults TLogSearchPager TLogLinksPanel TLogVirtualSearchResults TLogVirtualLinksPanel TLogScrollbar TLogMinimap
Experimental@simon_he/vue-tui/agentTAgentTerminalGraphic TAgentTranscript TMermaid TMermaidText TThinkingView TUserMessageView TToolCallView TToolLogView TVirtualMarkdown TVirtualList TRenderPlane 和 agent/console 常用基础组件
Experimental@simon_he/vue-tui/agent/mermaidTMermaid TMermaidImage TMermaidText TBeautifulMermaidText beautifulMermaidRenderer createBeautifulMermaidRenderer

下面的组件速读按用途分组,不代表 root entrypoint 导出。每个组件的 primary import 以生成的 组件 API 为准。

组件速读

类别组件典型用途适配性判断
RootTerminalProvider创建 terminal / renderer / event manager 上下文通用,适合所有宿主
LayoutTBox TView TAnchor TFlex TFlexItem TFlow TRenderLayer TRenderPlane布局、裁剪、层级、分层组合通用,和 CLI 业务无关
Text / ActionTText TLink TLinkifyText TMermaid TMermaidImage TMermaidText TBadge TTag TDivider TCode TKeyHint TTransition文本渲染、链接操作、结构图、徽标、分隔符通用
Input / FormTInput TInputBox TAutocompleteInput TCheckbox TFormField TPasswordInput TRadioGroup TSlider TSwitch TJsonEditorprompt、表单、结构化文本编辑通用,但推荐把补全/校验放到插件层
Data / TreeTTable TDataTable TTree多列数据、排序过滤、层级选择通用
ChartsTContributionGraph TLineChart TCandlestickChart TPieChart贡献热力图、趋势、K 线、占比图通用,适合 dashboard / agent token usage
PickersTCommandPalette TList TVirtualList TTranscriptView TLogView TLogSearchBar TLogSearchResults TLogSearchPager TLogLinksPanel TLogScrollbar TLogMinimap TSelect TPathPickerpalette、列表、transcript、日志、路径选择TPathPicker 本体可复用,路径语义由 provider 注入
OverlayTDialog TContextMenu TPopover TTooltip TToastViewport TMultilineModal TDebugOverlay对话框、菜单、提示、toast、详情查看、调试覆盖层通用,适合多种宿主
NavigationTBreadcrumb TStatusBar TTabs TSplitPane TRouterView + createTerminalRouter()路径导航、状态栏、多页面 TUI / shell通用
MediaTVideoterminal video、动态 ASCII 降级Experimental;按终端图形能力自适应
Agent ChromeTAgentTerminalGraphic TThinkingView TUserMessageView TToolCallViewterminal 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 仍是普通可变对象;如果要触发依赖它的组件重新绘制,推荐替换整个对象,而不是原地修改字段。

TThinkingViewTUserMessageViewTToolCallView 这种 agent/console 组件保留通用数据边界:只接收 titlecontentstatuscollapsedsuffixpreview 等渲染语义,不接收 provider/session/tool schema。默认样式对齐 best-agent CLI 的 transcript chrome;宿主可以用各组件的 styleheaderStylecontentStyletitleStylesuffixStylepreviewStyle 等 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 的复制提示 UI
  • clipboard (ClipboardApi?): 给 selection auto-copy 注入 clipboard;不传时 browser 使用运行时 clipboard
  • inputPlugins (TInputPlugin[]): 给子树里的 TInput / TInputBox 注入宿主插件(例如 terminal clipboard、TTY 风格快捷键);init-only,修改后需重新挂载 provider/input
  • pathPickerProvider (PathPickerProvider?): 给子树里的 TPathPicker 注入宿主路径 provider
  • linkOpener (TerminalLinkOpener | function?): 给 TLink openMode="host" 注入外部链接打开能力;浏览器 TerminalProvider 默认使用 window.open,CLI/headless 需要通过 createTerminalApp({ linkOpener }) 显式提供
  • theme (TuiThemeOverrides?): 组件主题 token 覆盖;传入 partial overrides 即可,例如 { colors: { link: "blueBright" } }TerminalProvider 会用 createTheme() 归一化;局部 style props 仍然优先
  • debugIme (boolean): 输出 IME 调试信息
  • debugTrace (boolean): 开启 trace(commit/event/focus)
  • domRendererOptions (DomRendererOptions?): DOM renderer 配置,例如 syncFlushMaxRows / syncFlushCellBudget;link options 会在更新时刷新,其他选项按 mount-time 使用,修改后需重新挂载 provider

Slots

  • default: 渲染你的 TUI 组件树

Example

vue
<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)。

可点击、可聚焦、可键盘激活的单行链接组件。除 disabled / openMode="none" 外,它会把 DOM-safe href 写入 Style.href,因此 DOM renderer links 开启时可以得到原生 anchor,CLI/stdout renderer 可以继续输出 OSC8 hyperlink。

默认 openMode="host":点击或按 Enter 时会先 emit activate,再调用 TerminalProvider.linkOpenercreateTerminalApp({ linkOpener }) 注入的 openExternal() 尝试打开;浏览器 TerminalProvider 默认使用 window.open,CLI/headless 不会默认执行系统命令。

openMode 语义:

  • host: emit activate,阻止 DOM native anchor 默认行为,并调用 linkOpener
  • event: emit activate,阻止组件处理的 DOM native anchor click,不调用 linkOpener;仍会写入 Style.href metadata,terminal OSC8 或 browser context menu 仍可能暴露 href,如需完全不输出 link metadata 请使用 none
  • native: click emit activate 并允许 renderer/native link activation;keyboard emit activate 后在有 linkOpener 时作为 terminal focus fallback 打开;如果 modifierClick 不满足,会阻止 native click
  • none: 只渲染文本,不写入 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 onActivateTLink 会尊重这个 defaultPrevented 状态,不再执行组件级 activate / host opener。组件化链接推荐由 TLink 自己拥有 activation。

Props

  • x/y (number, required)
  • w (number?): 不传时按 label || href 的 cell 宽度计算
  • h (number): 命中区域高度,默认 1
  • href (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
vue
<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?)
vue
<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 / TMermaidImage / TMermaidText

Mermaid 组件详见下方 TMermaidTextTMermaidImage。这里仅作为 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-mermaidTMermaid(adaptive)支持图片的终端出图,否则回退 ANSI / 源码;图片模式 TMermaidImage 走 Kitty/iTerm2 图形协议,需要额外安装 @resvg/resvg-js

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.protocolcontext.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" 可用于普通图片。

虚拟滚动或重内容场景建议使用 deferRenderUntilVisiblesuspendRenderWhileScrolling / suspendedcreateTerminalGraphicRenderQueue() 控制懒渲染、取消、缓存与并发。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
vue
<TAgentTerminalGraphic
  :x="0"
  :y="2"
  :w="72"
  :h="12"
  kind="math"
  content="\\frac{a}{b}"
  fallback="a / b"
  :renderer="terminalMathRenderer"
/>

TVideo

TVideo 是实验性的 terminal video 组件。组件本身保持 browser-safe;Node 侧 FFmpeg 和 yt-dlp adapter 从 @simon_he/vue-tui/experimental/video/node 单独导入,并在真正开始播放时才动态加载 node:child_process。系统需要能执行 ffmpeg;解析 YouTube 等视频页面时还需要 yt-dlp。两者都可以通过对应的 path option 指定可执行文件,不会在安装包时下载二进制。按照当前 yt-dlp EJS 安装说明,完整 YouTube 支持还需要 yt-dlp-ejs 和受支持的 JavaScript runtime;Demo 推荐安装默认启用的 Deno 2.3+。

src 可以是本地路径、file: URL 或 http(s) 视频链接。FFmpeg 先按 maxFps 降帧,再用 fast bilinear 缩放并 pad 到受控尺寸。Kitty / iTerm2 使用 PNG image2pipe,默认 12fps,自动像素尺寸会保持 cell 区域比例并限制在 640×360;普通 Unicode terminal 使用 gray8 raw frame,最高 10fps,并映射为纯 ASCII 文本,不执行 PNG 压缩、base64 转换或图形 escape sequence。ASCII 路径按 terminal cell 约 1:2 的宽高比采样:每行请求约一半 cell 数的灰度样本,再将每个亮度 glyph 横向绘制两次,从而保持视频比例。帧泵只保留 latest frame,被覆盖的帧不会进入渲染阶段;连续重复帧也会在转换前去重。暂停、隐藏、离开 viewport、滚动期间和卸载都会 abort decoder;恢复时 adapter 使用最后一帧的近似时间戳继续 seek。

开启 controls 后,点击视频画面会切换暂停/继续。controlsLayout 支持 "compact" | "cinema",默认为 "compact"compact 把播放/暂停、进度条和 1×/2×/3× 倍速放在底部 1 cell 的单行控制栏,cinema 使用底部 2 cells 的双行布局。鼠标可以拖动进度条;聚焦视频区域后可用 Enter 播放/暂停、Up/Down 切换倍速,CLI 下也支持 Space 和数字 1/2/3。进度拖动期间只更新预览并停止旧 decoder,松手后才按目标时间启动一次新 decoder;暂停状态 seek 只解码一帧作为预览。pausedplaybackRate 不绑定时由组件内部维护,使用 v-model:paused / v-model:playback-rate 时则由父组件控制。

可 seek 的进度条需要 durationMs,或由 frame source 在帧上提供 durationMsTVideoFrameSourceContext 同时包含 startAtMsplaybackRateloop;自定义 source 应据此定位媒体时间和控制播放速度。FFmpeg 在 2×/3× 时仍按墙钟限制到 maxFps,不会成倍增加 PNG/ASCII 转换;yt-dlp adapter 会短期复用已经成功解析的媒体直链,暂停、seek 和换速不会重复启动 yt-dlp。

TVideoFrameSourceContext.preferredFormat 会请求 pnggray8,自定义 frame source 应按请求返回对应帧。Kitty 使用固定 image/placement id,并由 stdout renderer 在同一 terminal frame 内完成旧帧清理和新帧绘制;没有图形协议时自动显示动态 ASCII 视频,Sixel video encoder 尚未接入。普通 HTTP MP4 需要服务端支持 Range,或使用把 moov 放在文件头的 faststart 文件。

ts
import { TVideo } from "@simon_he/vue-tui/experimental";
import { createFfmpegVideoFrameSource } from "@simon_he/vue-tui/experimental/video/node";

const videoFrames = createFfmpegVideoFrameSource();

h(TVideo, {
  x: 0,
  y: 0,
  w: 60,
  h: 18,
  src: "https://example.com/video.mp4",
  frameSource: videoFrames,
  maxFps: 12,
  controls: true,
  controlsLayout: "compact",
  durationMs: 60_000,
});

直播源应使用 createFfmpegVideoFrameSource({ live: true }),避免 FFmpeg 的 input readrate 对实时流产生丢包。adapter 只允许本地 filehttp(s) 输入协议,且始终使用 shell: falsesrc 作为单独 argv 传递。

YouTube watch URL 是网页而不是媒体流,不能直接交给 FFmpeg。createYtDlpVideoFrameSource 会先让 yt-dlp 解析一个 video-only HTTP(S) 流,再把临时 URL 和必要的 HTTP headers 交给同一 FFmpeg 管线。默认会根据 TVideo 的实际解码尺寸和 maxFps 自适应选择 source;常见终端区域通常选择 360p、至多 30fps 的源,再由 FFmpeg 降到最终 PNG/ASCII 尺寸和播放帧率。maxSourceHeight 只是可选上限。暂停、滚动或卸载时,解析进程与 FFmpeg 都会被终止。只应播放你有权访问和再利用的内容。

ts
import { createYtDlpVideoFrameSource } from "@simon_he/vue-tui/experimental/video/node";

const youtubeFrames = createYtDlpVideoFrameSource({
  ytDlpPath: "yt-dlp",
});

h(TVideo, {
  x: 0,
  y: 0,
  w: 60,
  h: 18,
  src: "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
  frameSource: youtubeFrames,
  maxFps: 12,
  controls: true,
  controlsLayout: "cinema",
});

pnpm run showcase 的 Video Tab 使用带 CC BY 3.0 署名的 Big Buck Bunny 短片和原生 HTMLVideoElement + Canvas 2Dpnpm run showcase:terminal 默认使用同一离线短片。准备好上面的 yt-dlp、EJS 和 runtime 后,可运行 VUE_TUI_YOUTUBE_DEMO=1 pnpm run showcase:terminal,直接解析 Blender 官方 YouTube 4K60 页面。三种路径都只在切入 Video Tab 后开始解码,离开 Tab 即取消并释放媒体资源。

TCommandPalette

命令面板组件,组合 TDialogTInput 和列表行渲染。默认按 label / detail / keywords 做 substring 过滤,也支持 v-model:query、custom matcher、async itemsProvider、group/separator 行和 closeOnSelectselect payload 包含 { item, index, sourceIndex, query, source },其中 index 是过滤/渲染后的 row index,sourceIndex 是原始 items 或 provider result index;默认选择命令不会自动关闭面板,宿主可在 @select 中更新 v-model,或显式开启 closeOnSelectitemsProvider rejection 会渲染错误行并 emit loadError

vue
<TCommandPalette
  v-model="paletteOpen"
  :items="commands"
  title="Command"
  placeholder="Search commands"
  @select="runCommand"
/>

TTable

TTable 是多列静态表格,负责列宽、表头、可选边框和 row click。它只从 rows 顶部按当前高度取可见行,不内置 scrollTop、分页或 virtualization;大数据浏览应组合外部分页/offset,或使用 TVirtualList 一类窗口化视图。

TDataTable

TDataTableTTable 上增加受控排序、过滤、行选择和受控 viewport offset;点击表头会 emit sortChange / update:sortBy / update:sortDirectionscrollTop 表示过滤/排序后结果集里的顶部可见行。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

vue
<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、每日/每轮计数密度这类紧凑历史视图。

vue
<TContributionGraph :x="0" :y="0" :values="dailyTokens" :rows="7" />

TLineChart

单色终端折线图组件,把 values 采样到给定 w/h 区域内,用 box-drawing glyph 绘制阶梯折线。适合 token 总量、延迟、吞吐等小型 dashboard 曲线。

vue
<TLineChart :x="0" :y="8" :w="40" :h="8" :values="tokenTotals" />

TCandlestickChart

K 线图组件,接收 { open, high, low, close } 数组并在宽度不足时保留最新 candles。默认上涨为绿色、下跌为红色,可通过 upStyledownStylewickStyle 覆盖。

vue
<TCandlestickChart :x="0" :y="17" :w="50" :h="10" :candles="prices" />

TPieChart

终端饼图组件,按 values 从顶部开始顺时针分段,用 segment styles 给不同区域着色。适合展示 token 分类、状态占比或简单资源分布。

vue
<TPieChart :x="44" :y="8" :w="16" :h="8" :values="[prompt, completion]" />

TTree

TTree 渲染层级节点,expandedIdsselectedId 都是受控状态。默认点击或 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 内容是否禁用由宿主组件控制。

vue
<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

TPasswordInputTInput secret 的轻量包装,输入值仍由宿主通过 v-model 控制,渲染时隐藏明文。

TAutocompleteInput

TAutocompleteInput 组合 TInput 和 suggestions 列表。静态 suggestions 保持宿主受控,也可以用 suggestionProvider(query, { signal }) 走异步路径;provider rejection 会渲染错误行并 emit loadError。选择 suggestion 时 emit update:modelValuechange 和包含 { value, index, sourceIndex, option, query, source }select,不 emit input

TForm

Advanced 表单上下文组件,从 @simon_he/vue-tui/vue 引入。它提供 validation/error context:model、同步 rulessubmitvalidation,以及 TFormField name 对错误文本的读取。自定义字段可以通过 /vueuseTForm()TFormContextKey 消费上下文。validate() 会在 submit 或宿主显式调用时更新 errors,不会在 model / rules 变化时自动重新校验。TForm 通过 template ref 暴露 validate()submit()clearValidation()setFieldError()disabled 会通过 form context 禁用当前已接入的内置表单控件(TCheckboxTSwitchTRadioGroupTSlider);TInputTPasswordInputTAutocompleteInput 仍需要宿主显式处理禁用语义。readOnly 目前只是通过 form context 提供给自定义字段消费者的提示,不会自动让后代输入控件只读。它不规定 schema 格式,也不拥有远程提交。

TContextMenu

TContextMenu 是轻量菜单 overlay,基于现有 TBox / TText / TView 渲染。默认会在外部点击时关闭;如需保持 non-modal 行为,可设置 closeOnOutside=falsex / y / w 是 caller-owned placement,组件不会自动贴边 clamp 或 flip。它不会直接操作系统 clipboard 或浏览器窗口;菜单项动作通过 select 交给宿主处理。

vue
<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,因此组件可以放在 TViewTBox 或 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 的底部状态栏。它是纯渲染组件,不注册全局快捷键。

vue
<TStatusBar :x="0" :y="23" :w="80" left="Ready" center="main" right="Ctrl+K" />

TBreadcrumb

TBreadcrumb 渲染路径导航,点击 item 只 emit select

vue
<TBreadcrumb :x="0" :y="0" :w="60" :items="pathSegments" @select="goToPath" />

TKeyHint

TKeyHint 渲染快捷键提示,不绑定或监听快捷键。

vue
<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 只提供默认样式;组件局部传入的 stylehoverStylefocusStyle 等 props 会覆盖主题。

ts
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 的布局。

TFlexItemwidth/heightw/hbasisminWidth/minHeightmaxWidth/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 可覆盖 paddingTFlexItem 支持 marginmarginX/marginY 和四个方向的 margin;margin 参与主轴分配和 wrap 判断,最终子内容仍渲染在 margin 内侧。

TFlexItem.order 控制视觉排列顺序;相同 order 的 item 保持原 slot 顺序。

需要内容固有尺寸时,可以在 TFlexItem.measure 中返回 preferred width/heightmeasure 会收到扣除 padding 后的 maxWidth/maxHeight 和当前 direction,返回值会在没有显式尺寸或 basis 时参与布局。

Props

  • TFlex: x/y/w/hdirectiongaprowGapcolumnGappaddingpaddingX/paddingYpaddingTop/paddingRight/paddingBottom/paddingLeftwrapalignItemsjustifyContentalignContentzIndex
  • TFlexItem: growshrinkbasiswidth/heightw/hminWidth/minHeightmaxWidth/maxHeightmeasureorderzIndexmarginmarginX/marginYmarginTop/marginRight/marginBottom/marginLeftalignSelf

Slots

  • TFlexItem.default: ({ rect }) => VNode

TInput

单行文本输入框(含光标、选择、剪贴板、IME 组合输入、快捷键),通过 v-model 管理值。

Props(常用)

  • x/y/w (number, required)
  • h (number):高度(默认 1
  • modelValue (string) + update:modelValue
  • placeholder (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.inputPluginscreateTerminalApp({ inputPlugins }) 或局部 plugins 注入宿主能力,而不是继续把平台差异写死到组件里。像 copy toast 这种 UI 反馈也应由宿主显式提供,不再依赖默认全局 hook。

Events(补充)

  • input / change / keydown / focus / blur
  • update:mentions / mentionClickcollectMentions=true 时(见 createPromptMentionPlugin()
  • update:multilineTexts / multilineClick:多行 token 相关
  • validationError:文本过滤/校验插件上报

TInput Plugins

用于扩展 TInput 的输入体验:通过 :plugins="[...]" 注入。插件列表是 init-only;如果需要切换宿主插件、path provider 或 prompt plugin,请重新挂载对应的 TInput

宿主级插件也可以统一从上层注入:

  • TerminalProvider.inputPlugins
  • createTerminalApp({ inputPlugins })
  • createTerminalApp({ clipboard }):只需要接入 clipboard 时的简化入口;传入 inputPlugins 时仍由宿主完全控制插件组合

TerminalProvider.inputPlugins 也是 init-only;已经挂载的 TInput 不会重新安装插件列表。

createTInputHostPlugin()

把宿主能力封装成 TInput 插件。适合注入:

  • readClipboardText / writeClipboardText
  • showToast
  • resolvePath / pathToHref
  • isTerminalLike

@simon_he/vue-tui 导出 browser-safe 的 createTInputHostPlugin()。CLI 侧的 defaultTInputHostPlugincreateDefaultTInputHostAdapter()@simon_he/vue-tui/cli 导出,负责 Node-like 的 clipboard / path 行为,不会自动附带 UI toast。如果宿主希望保留 Copied / Copy failed 这类提示,需要显式提供 showToast

createOsc52ClipboardProvider() 可作为 terminal clipboard 写入 provider 显式传给 createTerminalApp({ clipboard })。它不会默认执行系统剪贴板命令。

一个最小宿主接线示例:

ts
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),用于限制字符集、替换非法字符或做整体校验。

  • 命中过滤/拒绝时会触发 TInputvalidationError 事件(payload 含 originalText/acceptedText 等)

TInputPluginsContextKey

高级宿主如果希望按子树组合/覆盖输入插件,可以直接用公开导出的 TInputPluginsContextKeyprovide/inject

  • 适合像 reference app 那样,在某个页面根节点统一补一个局部 host plugin
  • 比起给每个 TInput 单独传 plugins,更适合做“页面级宿主接线”

TInputBox

带边框的输入框组合组件(内部是 TBox + TInput)。

Props(常用)

  • x/y/w/h (number, required)
  • modelValue (string) + update:modelValue
  • title (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):同长度内容更新时可递增,触发 repaint
  • modelValue (number) + update:modelValue
  • style (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:

  • TList wheel is viewport-only.
  • TList wheel no longer updates modelValue.
  • TList wheel no longer moves the active selection.
  • TList wheel burst applies only the final scrollTop in the next frame and emits scroll at most once per frame.
  • TList treats update:modelValue as selection-change, not selection-confirm.
  • Enter and double click emit change; they do not emit update:modelValue when committing the already-active item.
  • Keyboard-driven and external-model-driven viewport changes no longer emit scroll.
  • TList scroll now represents viewport-driven scroll changes, especially wheel scrolling and programmatic clamp.
  • onScroll is a result notification, not a veto/cancel hook.
  • Same-length item text changes require replacing the items array reference or bumping itemVersion.

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:

vue
<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 modelValue synchronization calls ensureActiveVisible()
  • 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:modelValue only when the active index changes.
  • Click emits update:modelValue only when the clicked index differs from the current active index.
  • Enter and double click emit change.
  • Enter and double click do not emit update:modelValue when 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:modelValue
  • scrollTop (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

getItemrenderItem 应保持稳定引用,数据变化用 itemVersion 通知组件。style / activeStyle 对象也应按 immutable 方式使用;样式变化时替换对象 identity。

ts
const items = markRaw(bigArray);
const itemVersion = ref(0);
const getItem = (index: number) => items[index];
const renderItem = (item: Row) => item.title;

避免在模板里传 inline function:

vue
<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:scrollTop
  • defaultScrollTop (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 or null
  • scroll: scroll metrics
  • update: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

数学公式:$...$ 块级公式在支持图形协议的终端(Kitty/Ghostty/WezTerm/iTerm2)渲染为图片,否则以 box 包裹原始 TeX;`$...--- title: Vue Terminal UI Components

description: Reference for Vue TUI components such as TerminalProvider, TBox, TInput, TList, TTable, TLogView, TVirtualMarkdown, and agent primitives.

Components 使用文档

本文档覆盖 @simon_he/vue-tui 当前内置的 Vue 组件,用于统一「渲染/参数/事件」的契约,便于实现一致的验收与测试。

坐标/尺寸单位:所有 x/y/w/h 均以「cell(字符格)」为单位,而不是像素。

完整的 Props/Events 列表请以自动生成文件为准:docs/generated/components-api.md(运行 pnpm run docs:gen 生成)。

导入入口

API maturityImport组件
Public@simon_he/vue-tuiTerminalProvider TBox TCommandPalette TDataTable TDialog TInput TLink TLinkifyText TList TSelect TTable TText TTree TView form helpers 和 TBadge/TTag/TDivider/TCode
Advanced@simon_he/vue-tui/vueTAnchor 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/markdownTMarkdownText TVirtualMarkdown
Public@simon_he/vue-tui/mermaidTMermaid TMermaidImage TMermaidText TBeautifulMermaidText beautifulMermaidRenderer createBeautifulMermaidRenderer
Experimental@simon_he/vue-tui/experimentalTCandlestickChart TContributionGraph TLineChart TPieChart TVideo TVirtualList TTranscriptView TLogView TLogSearchBar TLogSearchResults TLogSearchPager TLogLinksPanel TLogVirtualSearchResults TLogVirtualLinksPanel TLogScrollbar TLogMinimap
Experimental@simon_he/vue-tui/agentTAgentTerminalGraphic TAgentTranscript TMermaid TMermaidText TThinkingView TUserMessageView TToolCallView TToolLogView TVirtualMarkdown TVirtualList TRenderPlane 和 agent/console 常用基础组件
Experimental@simon_he/vue-tui/agent/mermaidTMermaid TMermaidImage TMermaidText TBeautifulMermaidText beautifulMermaidRenderer createBeautifulMermaidRenderer

下面的组件速读按用途分组,不代表 root entrypoint 导出。每个组件的 primary import 以生成的 组件 API 为准。

组件速读

类别组件典型用途适配性判断
RootTerminalProvider创建 terminal / renderer / event manager 上下文通用,适合所有宿主
LayoutTBox TView TAnchor TFlex TFlexItem TFlow TRenderLayer TRenderPlane布局、裁剪、层级、分层组合通用,和 CLI 业务无关
Text / ActionTText TLink TLinkifyText TMermaid TMermaidImage TMermaidText TBadge TTag TDivider TCode TKeyHint TTransition文本渲染、链接操作、结构图、徽标、分隔符通用
Input / FormTInput TInputBox TAutocompleteInput TCheckbox TFormField TPasswordInput TRadioGroup TSlider TSwitch TJsonEditorprompt、表单、结构化文本编辑通用,但推荐把补全/校验放到插件层
Data / TreeTTable TDataTable TTree多列数据、排序过滤、层级选择通用
ChartsTContributionGraph TLineChart TCandlestickChart TPieChart贡献热力图、趋势、K 线、占比图通用,适合 dashboard / agent token usage
PickersTCommandPalette TList TVirtualList TTranscriptView TLogView TLogSearchBar TLogSearchResults TLogSearchPager TLogLinksPanel TLogScrollbar TLogMinimap TSelect TPathPickerpalette、列表、transcript、日志、路径选择TPathPicker 本体可复用,路径语义由 provider 注入
OverlayTDialog TContextMenu TPopover TTooltip TToastViewport TMultilineModal TDebugOverlay对话框、菜单、提示、toast、详情查看、调试覆盖层通用,适合多种宿主
NavigationTBreadcrumb TStatusBar TTabs TSplitPane TRouterView + createTerminalRouter()路径导航、状态栏、多页面 TUI / shell通用
MediaTVideoterminal video、动态 ASCII 降级Experimental;按终端图形能力自适应
Agent ChromeTAgentTerminalGraphic TThinkingView TUserMessageView TToolCallViewterminal 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 仍是普通可变对象;如果要触发依赖它的组件重新绘制,推荐替换整个对象,而不是原地修改字段。

TThinkingViewTUserMessageViewTToolCallView 这种 agent/console 组件保留通用数据边界:只接收 titlecontentstatuscollapsedsuffixpreview 等渲染语义,不接收 provider/session/tool schema。默认样式对齐 best-agent CLI 的 transcript chrome;宿主可以用各组件的 styleheaderStylecontentStyletitleStylesuffixStylepreviewStyle 等 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 的复制提示 UI
  • clipboard (ClipboardApi?): 给 selection auto-copy 注入 clipboard;不传时 browser 使用运行时 clipboard
  • inputPlugins (TInputPlugin[]): 给子树里的 TInput / TInputBox 注入宿主插件(例如 terminal clipboard、TTY 风格快捷键);init-only,修改后需重新挂载 provider/input
  • pathPickerProvider (PathPickerProvider?): 给子树里的 TPathPicker 注入宿主路径 provider
  • linkOpener (TerminalLinkOpener | function?): 给 TLink openMode="host" 注入外部链接打开能力;浏览器 TerminalProvider 默认使用 window.open,CLI/headless 需要通过 createTerminalApp({ linkOpener }) 显式提供
  • theme (TuiThemeOverrides?): 组件主题 token 覆盖;传入 partial overrides 即可,例如 { colors: { link: "blueBright" } }TerminalProvider 会用 createTheme() 归一化;局部 style props 仍然优先
  • debugIme (boolean): 输出 IME 调试信息
  • debugTrace (boolean): 开启 trace(commit/event/focus)
  • domRendererOptions (DomRendererOptions?): DOM renderer 配置,例如 syncFlushMaxRows / syncFlushCellBudget;link options 会在更新时刷新,其他选项按 mount-time 使用,修改后需重新挂载 provider

Slots

  • default: 渲染你的 TUI 组件树

Example

vue
<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)。

可点击、可聚焦、可键盘激活的单行链接组件。除 disabled / openMode="none" 外,它会把 DOM-safe href 写入 Style.href,因此 DOM renderer links 开启时可以得到原生 anchor,CLI/stdout renderer 可以继续输出 OSC8 hyperlink。

默认 openMode="host":点击或按 Enter 时会先 emit activate,再调用 TerminalProvider.linkOpenercreateTerminalApp({ linkOpener }) 注入的 openExternal() 尝试打开;浏览器 TerminalProvider 默认使用 window.open,CLI/headless 不会默认执行系统命令。

openMode 语义:

  • host: emit activate,阻止 DOM native anchor 默认行为,并调用 linkOpener
  • event: emit activate,阻止组件处理的 DOM native anchor click,不调用 linkOpener;仍会写入 Style.href metadata,terminal OSC8 或 browser context menu 仍可能暴露 href,如需完全不输出 link metadata 请使用 none
  • native: click emit activate 并允许 renderer/native link activation;keyboard emit activate 后在有 linkOpener 时作为 terminal focus fallback 打开;如果 modifierClick 不满足,会阻止 native click
  • none: 只渲染文本,不写入 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 onActivateTLink 会尊重这个 defaultPrevented 状态,不再执行组件级 activate / host opener。组件化链接推荐由 TLink 自己拥有 activation。

Props

  • x/y (number, required)
  • w (number?): 不传时按 label || href 的 cell 宽度计算
  • h (number): 命中区域高度,默认 1
  • href (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
vue
<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?)
vue
<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 / TMermaidImage / TMermaidText

Mermaid 组件详见下方 TMermaidTextTMermaidImage。这里仅作为 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-mermaidTMermaid(adaptive)支持图片的终端出图,否则回退 ANSI / 源码;图片模式 TMermaidImage 走 Kitty/iTerm2 图形协议,需要额外安装 @resvg/resvg-js

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.protocolcontext.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" 可用于普通图片。

虚拟滚动或重内容场景建议使用 deferRenderUntilVisiblesuspendRenderWhileScrolling / suspendedcreateTerminalGraphicRenderQueue() 控制懒渲染、取消、缓存与并发。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
vue
<TAgentTerminalGraphic
  :x="0"
  :y="2"
  :w="72"
  :h="12"
  kind="math"
  content="\\frac{a}{b}"
  fallback="a / b"
  :renderer="terminalMathRenderer"
/>

TVideo

TVideo 是实验性的 terminal video 组件。组件本身保持 browser-safe;Node 侧 FFmpeg 和 yt-dlp adapter 从 @simon_he/vue-tui/experimental/video/node 单独导入,并在真正开始播放时才动态加载 node:child_process。系统需要能执行 ffmpeg;解析 YouTube 等视频页面时还需要 yt-dlp。两者都可以通过对应的 path option 指定可执行文件,不会在安装包时下载二进制。按照当前 yt-dlp EJS 安装说明,完整 YouTube 支持还需要 yt-dlp-ejs 和受支持的 JavaScript runtime;Demo 推荐安装默认启用的 Deno 2.3+。

src 可以是本地路径、file: URL 或 http(s) 视频链接。FFmpeg 先按 maxFps 降帧,再用 fast bilinear 缩放并 pad 到受控尺寸。Kitty / iTerm2 使用 PNG image2pipe,默认 12fps,自动像素尺寸会保持 cell 区域比例并限制在 640×360;普通 Unicode terminal 使用 gray8 raw frame,最高 10fps,并映射为纯 ASCII 文本,不执行 PNG 压缩、base64 转换或图形 escape sequence。ASCII 路径按 terminal cell 约 1:2 的宽高比采样:每行请求约一半 cell 数的灰度样本,再将每个亮度 glyph 横向绘制两次,从而保持视频比例。帧泵只保留 latest frame,被覆盖的帧不会进入渲染阶段;连续重复帧也会在转换前去重。暂停、隐藏、离开 viewport、滚动期间和卸载都会 abort decoder;恢复时 adapter 使用最后一帧的近似时间戳继续 seek。

开启 controls 后,点击视频画面会切换暂停/继续。controlsLayout 支持 "compact" | "cinema",默认为 "compact"compact 把播放/暂停、进度条和 1×/2×/3× 倍速放在底部 1 cell 的单行控制栏,cinema 使用底部 2 cells 的双行布局。鼠标可以拖动进度条;聚焦视频区域后可用 Enter 播放/暂停、Up/Down 切换倍速,CLI 下也支持 Space 和数字 1/2/3。进度拖动期间只更新预览并停止旧 decoder,松手后才按目标时间启动一次新 decoder;暂停状态 seek 只解码一帧作为预览。pausedplaybackRate 不绑定时由组件内部维护,使用 v-model:paused / v-model:playback-rate 时则由父组件控制。

可 seek 的进度条需要 durationMs,或由 frame source 在帧上提供 durationMsTVideoFrameSourceContext 同时包含 startAtMsplaybackRateloop;自定义 source 应据此定位媒体时间和控制播放速度。FFmpeg 在 2×/3× 时仍按墙钟限制到 maxFps,不会成倍增加 PNG/ASCII 转换;yt-dlp adapter 会短期复用已经成功解析的媒体直链,暂停、seek 和换速不会重复启动 yt-dlp。

TVideoFrameSourceContext.preferredFormat 会请求 pnggray8,自定义 frame source 应按请求返回对应帧。Kitty 使用固定 image/placement id,并由 stdout renderer 在同一 terminal frame 内完成旧帧清理和新帧绘制;没有图形协议时自动显示动态 ASCII 视频,Sixel video encoder 尚未接入。普通 HTTP MP4 需要服务端支持 Range,或使用把 moov 放在文件头的 faststart 文件。

ts
import { TVideo } from "@simon_he/vue-tui/experimental";
import { createFfmpegVideoFrameSource } from "@simon_he/vue-tui/experimental/video/node";

const videoFrames = createFfmpegVideoFrameSource();

h(TVideo, {
  x: 0,
  y: 0,
  w: 60,
  h: 18,
  src: "https://example.com/video.mp4",
  frameSource: videoFrames,
  maxFps: 12,
  controls: true,
  controlsLayout: "compact",
  durationMs: 60_000,
});

直播源应使用 createFfmpegVideoFrameSource({ live: true }),避免 FFmpeg 的 input readrate 对实时流产生丢包。adapter 只允许本地 filehttp(s) 输入协议,且始终使用 shell: falsesrc 作为单独 argv 传递。

YouTube watch URL 是网页而不是媒体流,不能直接交给 FFmpeg。createYtDlpVideoFrameSource 会先让 yt-dlp 解析一个 video-only HTTP(S) 流,再把临时 URL 和必要的 HTTP headers 交给同一 FFmpeg 管线。默认会根据 TVideo 的实际解码尺寸和 maxFps 自适应选择 source;常见终端区域通常选择 360p、至多 30fps 的源,再由 FFmpeg 降到最终 PNG/ASCII 尺寸和播放帧率。maxSourceHeight 只是可选上限。暂停、滚动或卸载时,解析进程与 FFmpeg 都会被终止。只应播放你有权访问和再利用的内容。

ts
import { createYtDlpVideoFrameSource } from "@simon_he/vue-tui/experimental/video/node";

const youtubeFrames = createYtDlpVideoFrameSource({
  ytDlpPath: "yt-dlp",
});

h(TVideo, {
  x: 0,
  y: 0,
  w: 60,
  h: 18,
  src: "https://www.youtube.com/watch?v=aqz-KE-bpKQ",
  frameSource: youtubeFrames,
  maxFps: 12,
  controls: true,
  controlsLayout: "cinema",
});

pnpm run showcase 的 Video Tab 使用带 CC BY 3.0 署名的 Big Buck Bunny 短片和原生 HTMLVideoElement + Canvas 2Dpnpm run showcase:terminal 默认使用同一离线短片。准备好上面的 yt-dlp、EJS 和 runtime 后,可运行 VUE_TUI_YOUTUBE_DEMO=1 pnpm run showcase:terminal,直接解析 Blender 官方 YouTube 4K60 页面。三种路径都只在切入 Video Tab 后开始解码,离开 Tab 即取消并释放媒体资源。

TCommandPalette

命令面板组件,组合 TDialogTInput 和列表行渲染。默认按 label / detail / keywords 做 substring 过滤,也支持 v-model:query、custom matcher、async itemsProvider、group/separator 行和 closeOnSelectselect payload 包含 { item, index, sourceIndex, query, source },其中 index 是过滤/渲染后的 row index,sourceIndex 是原始 items 或 provider result index;默认选择命令不会自动关闭面板,宿主可在 @select 中更新 v-model,或显式开启 closeOnSelectitemsProvider rejection 会渲染错误行并 emit loadError

vue
<TCommandPalette
  v-model="paletteOpen"
  :items="commands"
  title="Command"
  placeholder="Search commands"
  @select="runCommand"
/>

TTable

TTable 是多列静态表格,负责列宽、表头、可选边框和 row click。它只从 rows 顶部按当前高度取可见行,不内置 scrollTop、分页或 virtualization;大数据浏览应组合外部分页/offset,或使用 TVirtualList 一类窗口化视图。

TDataTable

TDataTableTTable 上增加受控排序、过滤、行选择和受控 viewport offset;点击表头会 emit sortChange / update:sortBy / update:sortDirectionscrollTop 表示过滤/排序后结果集里的顶部可见行。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

vue
<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、每日/每轮计数密度这类紧凑历史视图。

vue
<TContributionGraph :x="0" :y="0" :values="dailyTokens" :rows="7" />

TLineChart

单色终端折线图组件,把 values 采样到给定 w/h 区域内,用 box-drawing glyph 绘制阶梯折线。适合 token 总量、延迟、吞吐等小型 dashboard 曲线。

vue
<TLineChart :x="0" :y="8" :w="40" :h="8" :values="tokenTotals" />

TCandlestickChart

K 线图组件,接收 { open, high, low, close } 数组并在宽度不足时保留最新 candles。默认上涨为绿色、下跌为红色,可通过 upStyledownStylewickStyle 覆盖。

vue
<TCandlestickChart :x="0" :y="17" :w="50" :h="10" :candles="prices" />

TPieChart

终端饼图组件,按 values 从顶部开始顺时针分段,用 segment styles 给不同区域着色。适合展示 token 分类、状态占比或简单资源分布。

vue
<TPieChart :x="44" :y="8" :w="16" :h="8" :values="[prompt, completion]" />

TTree

TTree 渲染层级节点,expandedIdsselectedId 都是受控状态。默认点击或 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 内容是否禁用由宿主组件控制。

vue
<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

TPasswordInputTInput secret 的轻量包装,输入值仍由宿主通过 v-model 控制,渲染时隐藏明文。

TAutocompleteInput

TAutocompleteInput 组合 TInput 和 suggestions 列表。静态 suggestions 保持宿主受控,也可以用 suggestionProvider(query, { signal }) 走异步路径;provider rejection 会渲染错误行并 emit loadError。选择 suggestion 时 emit update:modelValuechange 和包含 { value, index, sourceIndex, option, query, source }select,不 emit input

TForm

Advanced 表单上下文组件,从 @simon_he/vue-tui/vue 引入。它提供 validation/error context:model、同步 rulessubmitvalidation,以及 TFormField name 对错误文本的读取。自定义字段可以通过 /vueuseTForm()TFormContextKey 消费上下文。validate() 会在 submit 或宿主显式调用时更新 errors,不会在 model / rules 变化时自动重新校验。TForm 通过 template ref 暴露 validate()submit()clearValidation()setFieldError()disabled 会通过 form context 禁用当前已接入的内置表单控件(TCheckboxTSwitchTRadioGroupTSlider);TInputTPasswordInputTAutocompleteInput 仍需要宿主显式处理禁用语义。readOnly 目前只是通过 form context 提供给自定义字段消费者的提示,不会自动让后代输入控件只读。它不规定 schema 格式,也不拥有远程提交。

TContextMenu

TContextMenu 是轻量菜单 overlay,基于现有 TBox / TText / TView 渲染。默认会在外部点击时关闭;如需保持 non-modal 行为,可设置 closeOnOutside=falsex / y / w 是 caller-owned placement,组件不会自动贴边 clamp 或 flip。它不会直接操作系统 clipboard 或浏览器窗口;菜单项动作通过 select 交给宿主处理。

vue
<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,因此组件可以放在 TViewTBox 或 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 的底部状态栏。它是纯渲染组件,不注册全局快捷键。

vue
<TStatusBar :x="0" :y="23" :w="80" left="Ready" center="main" right="Ctrl+K" />

TBreadcrumb

TBreadcrumb 渲染路径导航,点击 item 只 emit select

vue
<TBreadcrumb :x="0" :y="0" :w="60" :items="pathSegments" @select="goToPath" />

TKeyHint

TKeyHint 渲染快捷键提示,不绑定或监听快捷键。

vue
<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 只提供默认样式;组件局部传入的 stylehoverStylefocusStyle 等 props 会覆盖主题。

ts
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 的布局。

TFlexItemwidth/heightw/hbasisminWidth/minHeightmaxWidth/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 可覆盖 paddingTFlexItem 支持 marginmarginX/marginY 和四个方向的 margin;margin 参与主轴分配和 wrap 判断,最终子内容仍渲染在 margin 内侧。

TFlexItem.order 控制视觉排列顺序;相同 order 的 item 保持原 slot 顺序。

需要内容固有尺寸时,可以在 TFlexItem.measure 中返回 preferred width/heightmeasure 会收到扣除 padding 后的 maxWidth/maxHeight 和当前 direction,返回值会在没有显式尺寸或 basis 时参与布局。

Props

  • TFlex: x/y/w/hdirectiongaprowGapcolumnGappaddingpaddingX/paddingYpaddingTop/paddingRight/paddingBottom/paddingLeftwrapalignItemsjustifyContentalignContentzIndex
  • TFlexItem: growshrinkbasiswidth/heightw/hminWidth/minHeightmaxWidth/maxHeightmeasureorderzIndexmarginmarginX/marginYmarginTop/marginRight/marginBottom/marginLeftalignSelf

Slots

  • TFlexItem.default: ({ rect }) => VNode

TInput

单行文本输入框(含光标、选择、剪贴板、IME 组合输入、快捷键),通过 v-model 管理值。

Props(常用)

  • x/y/w (number, required)
  • h (number):高度(默认 1
  • modelValue (string) + update:modelValue
  • placeholder (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.inputPluginscreateTerminalApp({ inputPlugins }) 或局部 plugins 注入宿主能力,而不是继续把平台差异写死到组件里。像 copy toast 这种 UI 反馈也应由宿主显式提供,不再依赖默认全局 hook。

Events(补充)

  • input / change / keydown / focus / blur
  • update:mentions / mentionClickcollectMentions=true 时(见 createPromptMentionPlugin()
  • update:multilineTexts / multilineClick:多行 token 相关
  • validationError:文本过滤/校验插件上报

TInput Plugins

用于扩展 TInput 的输入体验:通过 :plugins="[...]" 注入。插件列表是 init-only;如果需要切换宿主插件、path provider 或 prompt plugin,请重新挂载对应的 TInput

宿主级插件也可以统一从上层注入:

  • TerminalProvider.inputPlugins
  • createTerminalApp({ inputPlugins })
  • createTerminalApp({ clipboard }):只需要接入 clipboard 时的简化入口;传入 inputPlugins 时仍由宿主完全控制插件组合

TerminalProvider.inputPlugins 也是 init-only;已经挂载的 TInput 不会重新安装插件列表。

createTInputHostPlugin()

把宿主能力封装成 TInput 插件。适合注入:

  • readClipboardText / writeClipboardText
  • showToast
  • resolvePath / pathToHref
  • isTerminalLike

@simon_he/vue-tui 导出 browser-safe 的 createTInputHostPlugin()。CLI 侧的 defaultTInputHostPlugincreateDefaultTInputHostAdapter()@simon_he/vue-tui/cli 导出,负责 Node-like 的 clipboard / path 行为,不会自动附带 UI toast。如果宿主希望保留 Copied / Copy failed 这类提示,需要显式提供 showToast

createOsc52ClipboardProvider() 可作为 terminal clipboard 写入 provider 显式传给 createTerminalApp({ clipboard })。它不会默认执行系统剪贴板命令。

一个最小宿主接线示例:

ts
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),用于限制字符集、替换非法字符或做整体校验。

  • 命中过滤/拒绝时会触发 TInputvalidationError 事件(payload 含 originalText/acceptedText 等)

TInputPluginsContextKey

高级宿主如果希望按子树组合/覆盖输入插件,可以直接用公开导出的 TInputPluginsContextKeyprovide/inject

  • 适合像 reference app 那样,在某个页面根节点统一补一个局部 host plugin
  • 比起给每个 TInput 单独传 plugins,更适合做“页面级宿主接线”

TInputBox

带边框的输入框组合组件(内部是 TBox + TInput)。

Props(常用)

  • x/y/w/h (number, required)
  • modelValue (string) + update:modelValue
  • title (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):同长度内容更新时可递增,触发 repaint
  • modelValue (number) + update:modelValue
  • style (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:

  • TList wheel is viewport-only.
  • TList wheel no longer updates modelValue.
  • TList wheel no longer moves the active selection.
  • TList wheel burst applies only the final scrollTop in the next frame and emits scroll at most once per frame.
  • TList treats update:modelValue as selection-change, not selection-confirm.
  • Enter and double click emit change; they do not emit update:modelValue when committing the already-active item.
  • Keyboard-driven and external-model-driven viewport changes no longer emit scroll.
  • TList scroll now represents viewport-driven scroll changes, especially wheel scrolling and programmatic clamp.
  • onScroll is a result notification, not a veto/cancel hook.
  • Same-length item text changes require replacing the items array reference or bumping itemVersion.

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:

vue
<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 modelValue synchronization calls ensureActiveVisible()
  • 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:modelValue only when the active index changes.
  • Click emits update:modelValue only when the clicked index differs from the current active index.
  • Enter and double click emit change.
  • Enter and double click do not emit update:modelValue when 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:modelValue
  • scrollTop (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

getItemrenderItem 应保持稳定引用,数据变化用 itemVersion 通知组件。style / activeStyle 对象也应按 immutable 方式使用;样式变化时替换对象 identity。

ts
const items = markRaw(bigArray);
const itemVersion = ref(0);
const getItem = (index: number) => items[index];
const renderItem = (item: Row) => item.title;

避免在模板里传 inline function:

vue
<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:scrollTop
  • defaultScrollTop (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 or null
  • scroll: scroll metrics
  • update: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

行内公式在图形终端混排进文字行(≤2 行高),超高公式(矩阵等)与无图形环境保持原始文本,格式错误的公式也会回退为原始文本。需要安装可选 peer mathjax-full + @resvg/resvg-js 才会启用图片路径。详见 Markdown 数学公式渲染

content string 路径仍然只做 per-frame coalescing:一帧内多次 append 会合并成一次 rebuild,但 rebuild 本身仍然会从当前 full markdown string parse。长文档 streaming transcript 场景可以使用 createMarkdownBlockSource(),在消息、tool fence 或代码块完成时 finalizeBlock(),再把 blocks 传给 TVirtualMarkdown,避免反复重 parse 已 finalize 的历史。

TMermaid

自适应 Mermaid 组件:默认显示 raw source,然后按终端能力自动升级到最佳呈现。

  • 终端支持 Kitty / iTerm2 图形协议 且 PNG rasterize 成功 → 显示图片(beautiful-mermaid SVG + @resvg/resvg-js)。
  • 不支持图片、或图片渲染失败 → 回退到 ANSI 文本图beautiful-mermaidrenderMermaidASCII)。
  • ANSI 也不可用 / 源码太复杂 / renderer 失败或超时 → 保持 raw source

Advanced import: @simon_he/vue-tui/vue

Agent import: @simon_he/vue-tui/agent

Beautiful Mermaid bridge import: @simon_he/vue-tui/mermaid

Agent Beautiful Mermaid bridge import: @simon_he/vue-tui/agent/mermaid

注意:@simon_he/vue-tui/vue@simon_he/vue-tui/agent 导出的 TMermaid / TMermaidText 是 renderer-agnostic primitive,不会自动 import beautiful-mermaid,也不检测图形能力。

@simon_he/vue-tui/mermaid@simon_he/vue-tui/agent/mermaid 导出的 TMermaidadaptive 默认:内置 beautiful-mermaid ANSI bridge(textRenderer)与图片 rasterizer,默认用 isSimpleMermaidFlowchartSource 限制 ANSI 回退只处理简单 flowchart,避免复杂图在非图片终端上出现对齐错乱。

流式场景沿用 source-first 策略:streaming=true && final=false 时只显示源码;final=true 后先尝试图片,失败再尝试 ANSI,都失败保持源码。

需要固定呈现方式时用专用组件:TMermaidText(纯 ANSI)、TMermaidImage(图片优先)。

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:

vue
<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:

vue
<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:

vue
<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 时才尝试渲染并在成功时原子替换 source
  • ascii (boolean):使用纯 ASCII 而不是 Unicode box drawing
  • options (TMermaidAsciiOptions?):传给 renderer 的 spacing/theme options;组件始终强制 colorMode: "none"
  • renderer (TMermaidRenderer?):自定义 renderer,适合测试或替换 Mermaid engine
  • shouldRenderSource (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 文案。

TMermaidImage

Mermaid 图片 primitive:把 mermaid 源码渲染成 PNG,通过 Kitty / iTerm2 图形协议直接画进真实终端。渲染管线镜像 KaTeX 数学公式图片的实现方式:beautiful-mermaidrenderMermaidSVGAsync,零 DOM 依赖)产出 SVG → @resvg/resvg-js 转 PNG → 组件用 getTerminalGraphicsOutput().queue() 在帧末尾写入 raw escape sequence。

依赖均为 optional peer,按需安装:pnpm add beautiful-mermaid @resvg/resvg-js

Import: @simon_he/vue-tui/mermaid / @simon_he/vue-tui/agent/mermaid(内置懒加载 rasterizer)。与 beautifulMermaidRenderer 一样,TMermaidImage 属于 mermaid bridge 入口,@simon_he/vue-tui/vue / @simon_he/vue-tui/agent 保持 renderer-agnostic,不包含 beautiful-mermaid 引用。

终端不支持图形协议、stdout 不是 TTY、在 tmux/screen/zellij 内未开 passthrough、或 rasterizer 缺失时,组件自动降级:传了 textRenderer 则尝试 ANSI 文本图,否则显示原始 mermaid 源码,不会报错。

Kitty 图形协议下支持缩放 + 平移:按住 Ctrl(macOS 浏览器里也可用 Cmd)+ 滚轮 → 以鼠标位置为中心放大/缩小,范围限制在容器区域内(越界裁剪,不撑破布局);放大后可以直接拖拽图片平移。缩放通过 a=p placement resize 实现,不重新发送 PNG,性能开销极小;iTerm2 没有 in-place resize 序列,自动禁用。不带修饰键的滚轮直接透传给外层滚动容器(浏览历史消息时经过 mermaid 不会卡住滚动)。关闭用 :zoom-on-wheel="false",改修饰键用 :zoom-modifier

点击图片区域或 header 上的 copy 按钮,会把完整 mermaid raw content 复制到剪贴板(触发 copy 事件)。

Example

vue
<script setup lang="ts">
import { TMermaidImage } from "@simon_he/vue-tui/agent/mermaid";

const diagram = `graph TD
  Prompt --> Plan
  Plan --> ToolCall
  ToolCall --> Answer`;
</script>

<template>
  <!-- h 省略时按图片宽高比自适应;不传时在 w 内等比缩放 -->
  <TMermaidImage :x="0" :y="0" :w="72" :content="diagram" />
</template>

自定义 rasterizer(例如走自己的 mermaid 渲染服务):

vue
<script setup lang="ts">
import { TMermaidImage } from "@simon_he/vue-tui/agent/mermaid";

const renderer = async (code, options) => {
  // 返回 { base64, widthCells, heightCells, naturalWidth?, naturalHeight? }
  // 或 null(保持源码显示)
  const png = await myMermaidService(code);
  return { base64: png.base64, widthCells: 40, heightCells: 12 };
};
</script>

<template>
  <TMermaidImage :x="0" :y="0" :w="72" :content="diagram" :renderer="renderer" />
</template>

Props

  • x/y/w (number, required):渲染区域左上角与宽度
  • h (number?):固定高度;不传时按图片高宽比自适应(rasterize 完成前按源码行数)
  • content / code (string?):Mermaid source;同时传入时 code 优先
  • final / streaming (boolean)streaming=true && final=false 时只显示源码,不触发 rasterize;结束后再出图
  • renderer (TuiMermaidImageRasterizer?):自定义 mermaid → PNG rasterizer;不传时用内置懒加载(beautiful-mermaid + @resvg/resvg-js)。传入时该组件直接调用此 rasterizer(不走全局缓存),可用于需要独立管线的场景
  • textRenderer (TMermaidRenderer?):ANSI 文本 renderer(与 TMermaidTextrenderer 同签名)。传了以后,终端不支持图片或图片渲染失败时回退为 ANSI 文本图;不传则回退为原始源码。TMermaid 自动注入 beautifulMermaidRenderer
  • shouldRenderSource (TMermaidRenderEligibility?):ANSI 回退的 eligibility guard;返回 false 时保持原始源码。TMermaid 默认注入 isSimpleMermaidFlowchartSource
  • textOptions (TMermaidAsciiOptions?):传给 textRenderer 的 spacing/theme options;组件强制 colorMode: "none"
  • cellWidthPx / cellHeightPx / scale (number?):PNG 像素 → cell 的换算参数(默认 8 / 16 / 2)
  • bg / fg (string?):传给 renderMermaidSVG 的主题色;fg 未传时从 terminal 默认样式解析
  • maxWidthCells / maxHeightCells (number?):限制图片最大占用格数,超范围等比缩放
  • padding (number?):SVG 画布内边距(默认 40px)
  • box (boolean, true):是否绘制外框(含 title 与 copy 按钮)
  • title (string, "mermaid"):header 标题
  • copyButton (boolean, true):是否显示 copy 按钮
  • copyOnClick (boolean, true):点击图片区域是否复制完整源码
  • copyText / copiedText (string):copy 按钮文案与复制成功文案
  • copiedDurationMs (number, 1200)copied 反馈持续时间
  • maxRenderSourceChars / maxRenderSourceLines:超大源码跳过 rasterize,直接显示源码
  • ascii (boolean):使用 ASCII 边框字符
  • zoomOnWheel (boolean, true):Kitty 图形协议下对图片做缩放 + 平移(以鼠标为中心,限制在容器区域内);iTerm2 无 in-place resize,自动禁用
  • zoomModifier ("meta" | "ctrl" | "metaCtrl" | "none", "metaCtrl"):缩放需要的修饰键;不带修饰键的滚轮透传给滚动容器。终端鼠标上报拿不到 metaKey,所以真实终端里用 Ctrl;浏览器里 Cmd / Ctrl 均可
  • minZoom / maxZoom (number, 1 / 6):缩放范围(相对 fit 尺寸的倍率)
  • zoomSensitivity (number, 0.002):缩放灵敏度(每像素 delta 的指数系数;滚轮一格约 ±22%)

Events

  • copy (payload: { text, ok, error? }):复制完整 mermaid raw content 后触发;text 即完整源码

模块级 rasterizer API

@simon_he/vue-tui/mermaid / @simon_he/vue-tui/agent/mermaid 导出,与 math-image 对齐:

  • getMermaidImage(code, options?):rasterize 并按 (code, cell metrics, colors) 缓存;in-flight 去重;完成时通知订阅者
  • getCachedMermaidImage(code, options?):同步缓存查询(未命中返回 null
  • loadMermaidImageRenderer():确保内置 rasterizer 已加载(或判定不可用),返回 boolean
  • isMermaidImageRendererReady():rasterizer 是否可用(组件据此门控,避免反复重试)
  • setMermaidImageRasterizer(rasterizer | null):注入自定义 rasterizer(传 null 恢复内置)
  • clearMermaidImageCache():清空 PNG 与失败缓存
  • subscribeMermaidImage(listener):图片完成后收到通知,用于重建布局
  • resolveMermaidSvgForResvg(svg, bg, fg):把 beautiful-mermaid 的 CSS 变量 / color-mix() 内联成 hex(resvg 不支持这些 CSS,否则 PNG 全黑)

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.href metadata。DOM renderer 默认不把 Style.href 渲染为原生 <a>。启用 links: truelinks: { 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 会对稳定 keyed blocks 的可见行做 row-local dirty diff。滚动默认仍 repaint viewport;当组件未裁剪、占满并独占 terminal rows,且当前 renderer 支持 scroll operations 时,可以显式设置 rowScrollMode="unsafe-full-row",让小幅滚动 shift plane 并只 repaint 新暴露行。可见 terminal graphics 会自动回退 viewport repaint。streaming append 不会自动 follow tail,仍保持 absolute scrollTop / 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> 会自动 unwrap
  • scrollTop (number?):受控 visual-row scrollTop;省略时由组件内部维护
  • defaultScrollTop (number?):非受控模式初始 visual-row scrollTop;省略时初始显示底部
  • style (Style?)
  • autoFocus (boolean)
  • autoStickToBottom (boolean):在底部时 append 自动贴底,默认 true
  • overscan (number)wrap=true 底部窗口预先测量的额外 visual rows,默认 2
  • wrap (boolean):长逻辑行按 cell width 拆成多个 visual rows,默认 false
  • visualIndexMode ("estimated" | "exact"):wrapped visual row 总数的索引模式,默认 "estimated"
  • visualIndexOptions (TLogViewVisualIndexOptions?)measureBudgetMs 默认 4maxMeasuredLines 可限制 retained-window exact scan 范围
  • ansi (boolean):解析每条 logical line 中的 ANSI SGR styling,默认 false
  • links (boolean)ansi=true 时解析 OSC8 hyperlinks,默认 false
  • linkStyle (Style?):link text 叠加样式;OSC8 links 默认保留日志原 ANSI 样式,仅叠加 { underline: true } 或传入的 linkStylelinkify=true 的自动链接会先叠加 theme.components.TLink.style 并遵循 TLink.underline=false
  • keyboardLinks (boolean):启用当前 visible OSC8 link 的键盘导航,默认 false
  • linkFocusStyle (Style):focused visible link 的叠加样式,默认 { inverse: true }
  • searchQuery (string):在当前 retained source window 内搜索 visible text,默认 ""
  • searchOptions (TLogViewSearchOptions?)mode 默认 "text"caseSensitive / wholeWord 默认 falsemaxMatches 默认 10_000scanBudgetMs 默认 4regexFlags 可追加 m / u / s 等 flags;maxMatchesPerLine 默认 1_000
  • highlightMatches (boolean):是否绘制 search matches,默认 true
  • matchStyle (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

ts
import { createAppendOnlyLogStore } from "@simon_he/vue-tui/experimental";

const log = createAppendOnlyLogStore({ maxLines: 10_000 });

log.appendChunk("hello");
log.appendChunk(" world\nnext line");
vue
<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 如果能提供稳定身份,建议实现:

ts
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 linkActivateEscape 清除当前 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 会追加 iregexFlags 可以传 m / u / s 等额外 flags。传入的 g 会被忽略,因为组件内部需要控制 lastIndexy 也会被忽略。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 求值。

vue
<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() 会增加。
  • 非受控模式:省略 scrollTopTLogView 内部维护滚动位置;defaultScrollTop 只在初始 mount 使用一次,省略时初始显示底部。
  • 受控模式:传入 scrollTop 并监听 update:scrollTopTLogView 会 emit 期望的 next scrollTop,但不会在父组件回写 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。scroll payload 和 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。
vue
<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 index
  • markers 可以直接来自 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 和父组件同步
vue
<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。它只消费父组件传入的 metricsmarkers 和可选 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
vue
<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
  • Enter emit nextShift+Enter emit previousEsc emit clear
  • click [T] / [R][Aa][W] 只 emit update events,不直接触发搜索
  • mode="regex"wholeWord toggle 会渲染为 disabled,并且 click 不会 emit update:wholeWord
  • state.status 可以驱动 Scanning…current/matchCountInvalid regex 等展示
vue
<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 交互
  • select payload 里的 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 emit select
  • activeIndex 是 external sync hint:click / keyboard 会先更新内部 active row,再 emit activeChange;父组件可在下一拍重新同步
  • results 应该是当前 page/window,通常长度不超过组件高度;组件不会自己 virtualize 整个结果集
  • 不要每一帧都对全部 10k matches 调 includePreview: true;应当只给当前页或当前窗口取 preview
vue
<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 search
  • scanning 显示 Scanning… 和当前已发现 match count
  • done + matchCount=0 显示 No matches
  • error 显示 Invalid regex
  • done 显示单行 pager,例如 ◀ 2/13 245 matches ▶
  • click / ArrowLeft / ArrowRight / PageUp / PageDown 都只 emit,父组件负责接到 composable 或 handle
  • previousPage / nextPagepageChange 是两套等价事件;通常只处理其中一套,避免重复翻页

useTLogSearchResultsPage 负责从 TLogViewHandle 拉取当前页结果、同步 page-local activeIndex、clamp 页码,并在 selectResult(matchIndex) 时调用 selectSearchMatch(matchIndex) 后刷新当前页状态。推荐把它和 TLogSearchResults / TLogSearchPager 一起使用,而不是在父组件里重复手写 offsetpageSizecurrentMatchIndex 同步逻辑。它的 pageSize / includePreview / previewWidth / contextCells 都是 setup-time configuration;如果这些值需要动态变化,请重新创建 controller/composable。

Search UX suite wiring

如果你希望把 TLogSearchBarTLogSearchResultsTLogSearchPagerTLogScrollbarTLogMinimap 一起接成完整搜索体验,推荐直接使用 useTLogSearchController。它会集中管理:

  • query / mode / caseSensitive / wholeWord
  • searchBarState
  • resultsPage
  • markers
  • metrics
  • searchHistory / savedSearches

useTLogSearchControllerpageSize / includePreview / previewWidth / contextCells / maxHistory / initialSavedSearches 都按 setup-time configuration 处理;如果这些值要动态变化,建议重建 controller。

vue
<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 link
  • Enter 只 emit activate,不会自动打开浏览器
  • links 应该是当前 viewport/window 内的 visible links;组件不会自己维护完整 retained-window 历史

如果你希望把 TLogView 的 visible OSC8 link 导航、panel 展示和应用层 action 统一起来,推荐直接使用 useTLogLinkController。它会集中管理:

  • visibleLinks
  • activeIndex
  • focusVisibleLink / focusNextLink / focusPreviousLink
  • activateVisibleLink / activateFocusedLink
  • handleLinkClick / 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。

vue
<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: searchQuery
  • search: { 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 [])
  • modelValue follows valueMode: index values by default, option values for value, option objects for option
  • multiple (boolean)
  • valueMode ('index'|'value'|'option')
  • optionProvider(query, { signal }) loads async options for the current query; stale aborted requests do not emit loadError
  • When using optionProvider, prefer valueMode="value" with stable option value fields. 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, renders loadingText; provider rejection renders errorText and emits loadError
  • multipleEmit ('label'|'value'|'index'|'both'); label emits labels, value emits option values, index emits indices, and both emits { indices, labels, values }
  • valueMode controls the modelValue shape. multipleEmit controls the change / confirm payload shape separately. For object options with value, set multipleEmit="value" if change / confirm should emit option values instead of labels.
  • searchable emits update:query from typed characters; it does not filter local options by itself
  • style/highlightStyle (Style?)
  • autoFocus (boolean)
  • closeOnBlur (boolean)

Events

  • change: single-select emits the selected label or null; valueMode only affects v-model
  • confirm: multi-select emits the payload selected by multipleEmit
  • loadError: { query, error } when the active async provider request rejects
  • close / focus / blur / keydown

TPathPicker

路径输入 + 自动补全(Tab completion),用于 CLI 场景选择文件路径。

Props

  • x/y/w/h (number, required)
  • workspace (string, required): 工作区绝对路径(用于解析与补全)
  • mode ('any'|'file'|'directory')
  • modelValue (string) + update:modelValue
  • placeholder (string?)
  • showHidden (boolean)
  • maxSuggestions (number)
  • provider (PathPickerProvider?): 局部覆盖路径 provider;也可通过 TerminalProvider.pathPickerProvidercreateTerminalApp({ 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:modelValue
  • w/h (number, required)
  • title (string) / padding (number)
  • zIndex (number) / style (Style?)
  • placementcenter|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right
  • offsetX/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 / keydown
  • confirm: { 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,并把 terminalscheduler.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 给 TRenderPlanekey,例如 <TRenderPlane :key="activePlane" :plane="activePlane">
  • 不要依赖动态修改 plane prop 来迁移已 mount subtree;tab switching、dialog migration、animation plane 迁移都应 key remount
  • frame tasks and scheduler invalidates default to the mounted plane, but an explicit plane field still escapes that default. Passing ctx.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.

Bug reports, feature requests, and documentation issues are tracked on GitHub Issues.