v1.1.0Pi v1.1.0 源码分析Analysis of Pi v1.1.0 · 2026-10-07

tuitui

差分渲染的终端 UI 库Terminal User Interface library with differential rendering for efficient text-based applications

pi-tui

← → 翻页 · F 全屏 · O 目录 · T 明暗 · L 语言← → navigate · F fullscreen · O overview · T theme · L language

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.001
01 · 组件模型01 · The component modelpi-tui

组件模型The component model

The component model: render(width) returns lines例:交互模式的组件树e.g. the interactive-mode tree每一帧的约定The contract for every frame内置组件Built-in components18 个,按用途18, by purpose«interface»Component+render(width): string[]+handleInput?(data): void+handleMouse?(event)+wantsKeyRelease?: boolean+invalidate(): void«interface»Focusable+focused: boolean«class»Container+children: Component[]+addChild() · removeChild() · clear()+render(w): concat(child.render(w))«interface»TUI extends Component+mode: "regular" | "fullscreen"+setFocus(c) · showOverlay(c, opts)+requestRender(force?) · renderNow()+… 25 members in totalTuiAltScreen根 = Containerroot = ContainerText标题与提示title & hintsScrollView消息列表(Markdown…)messages (Markdown…)Container扩展 widgetsextension widgetsEditorfocused = trueText页脚:模型 · tokenfooter: model · tokensboxstackh-stackv-stackspacerscroll-viewmouse-regiontexttruncated-textmarkdownimageinputeditorselect-listsettings-listloadercancellable-loaderalt-screen-flashrender(width) 自上而下,各子组件的行拼在一起render(width) top-down; child lines are concatenated每行可见宽度 ≤ widthEvery line's visible width ≤ width否则抛错,并写 pi-tui-crash.logotherwise it throws and writes pi-tui-crash.logfocused 组件在光标处输出 CURSOR_MARKERThe focused component emits CURSOR_MARKER"\x1b_pi:c\x07":零宽 APC 序列"\x1b_pi:c\x07": a zero-width APC sequenceTUI 找到标记并删掉,再移动硬件光标TUI finds and strips it, then moves the hardware cursor输入法候选框因此跟着光标走so IME candidate windows follow the cursor每行末尾追加 SEGMENT_RESETEach line gets SEGMENT_RESET appended\x1b[0m\x1b]8;;\x07:样式和超链接不会串行\x1b[0m\x1b]8;;\x07: no style or link bleeds to the next lineinvalidate():主题变化时清缓存invalidate(): drop caches on theme change下一次 render 从头计算the next render starts from scratch布局layout内容content输入input反馈feedback12345接口 / 组件interfaces / components获得焦点focused约定contract

每一行的可见宽度不能超过 width;超过时 TUI 抛错并写 pi-tui-crash.log,而不是让终端自动换行弄乱画面。No line may be wider than width; if one is, the TUI throws and writes pi-tui-crash.log instead of letting the terminal wrap and corrupt the screen.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.002
02 · 差分渲染02 · Differential renderingpi-tui

差分渲染Differential rendering

  • 宽度变化、高度变化(Termux 除外)、内容变短(开启 clearOnShrink 时)以及变化发生在视口上方时,会清屏重绘——终端滚动区里已经滚出去的行无法原地修改。A width change, a height change (except on Termux), shrinking content (with clearOnShrink) or a change above the viewport triggers a full redraw — lines already scrolled into the terminal's history cannot be edited in place.
  • 只删了行时不重写任何内容,只清掉尾部多出来的行。If lines were only deleted, nothing is rewritten; the extra tail lines are just cleared.
Differential rendering: what follows requestRender()调度Scheduling合并请求 · 每帧至少间隔 16mscoalesced · at least 16 ms between frames准备一帧(两种屏幕共用)Preparing a frame (both screens)TuiMainScreen.doRender()regular 模式:在终端滚动区里原地更新regular mode: update in place in the scrollbackrequestRender()已请求就直接返回no-op if already pendingnextTick同一 tick 内合并coalesce this tickscheduleRender()setTimeout(16ms − 距上一帧)setTimeout(16 ms − since last)doRender()子类实现:per subclass:main / alt screenhandleInput(data)requestImmediateRender()nextTick 直接渲染,取消等待中的定时器render on nextTick, cancel any pending timerrender(width)组件树 → 行tree → linescompositeOverlays覆盖层拼进行内splice overlays inextractCursorPosition找到并删掉标记find & strip markerapplyLineResets行尾 SEGMENT_RESETSEGMENT_RESET per line宽度检查width check超宽 → 抛错too wide → throw首帧?first frame?previousLines = []全量输出full renderfullRender(false)宽度变了?width changed?previousWidth清屏重绘clear + redrawfullRender(true)高度变了?height changed?Termux 除外not on Termux清屏重绘clear + redrawfullRender(true)内容变短?shrank?clearOnShrink清屏重绘clear + redrawfullRender(true)没有变化?nothing changed?逐行比较compare lines只移光标move cursor only只删了行?only deletions?first ≥ new.length清掉尾部行clear tail lines在视口上方?above viewport?first < viewportTop清屏重绘clear + redrawfullRender(true)否则:局部重写else: partial writefirstChanged … lastChanged\x1b[?2026h … ?2026l定时器到期timer fires按键延迟敏感keys are latency-sensitive比较前把变化区间扩大到覆盖整张 Kitty 图片;所有输出都包在同步输出 ?2026h … ?2026l 里,避免闪烁。The changed range is widened to cover whole Kitty images; every write is wrapped in synchronized output ?2026h … ?2026l to avoid flicker.fullRender(true) = 清屏 + 清滚动区(\x1b[2J\x1b[H\x1b[3J)后输出全部行;TUI.fullRedraws 计数。fullRender(true) = clear screen and scrollback (\x1b[2J\x1b[H\x1b[3J), then write every line; counted in TUI.fullRedraws.否 → 下一个判断no → next check是 → 全量重绘yes → full redraw是 → 轻量处理yes → light path
图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.003
03 · 两种屏幕:regular 与 fullscreen03 · Two screens: regular and fullscreenpi-tui

两种屏幕:regular 与 fullscreenTwo screens: regular and fullscreen

Two screens: regular (main) and fullscreen (alternate)TuiMainScreenmode = "regular"TuiAltScreenmode = "fullscreen" · 进入 \x1b[?1049hmode = "fullscreen" · enters \x1b[?1049hrenderLayoutFrame(root)布局帧:盒子 · 滚动视图layout frame: boxes · scroll viewsapplySearchHighlights搜索命中高亮search hitscompositeScrollToEndIndicator"回到底部"提示"jump to end" pillcompositeOverlays覆盖层overlaysapplySelection鼠标选区mouse selectioncompositeFlashes短暂提示transient flashesextractCursorPosition · applyLineResets光标 · 行尾重置 · 裁到 widthcursor · resets · clip to width与 previousScreen 逐行比较diff rows against previousScreen尺寸变化 → 整屏重绘size change → full redraw\x1b[row;1H\x1b[2K …只写变化的行 · 包在 ?2026 中changed rows only · inside ?2026内容写进终端自己的滚动区Content goes into the terminal's own scrollback往上翻就是历史,退出后仍在scroll up for history; it stays after exit按行差分(见上一张图)Line diff (see the previous figure)只重写 firstChanged…lastChangedrewrites only firstChanged…lastChanged不抓鼠标No mouse capture终端原生选择与复制照常native selection and copy still work视口之上的变化只能清屏重绘Changes above the viewport force a full redrawrows/cols 变化同理so do rows / cols changesKitty 图片按行预留Kitty images reserve rows变化区间会扩到整张图diff ranges widen to whole images选择CHOOSING# pi-coding-agent: settings.json"tuiMode": "regular" | "fullscreen"# 默认 fullscreen(getTuiMode)# default: fullscreen (getTuiMode)鼠标mouse?1000 ?1002 ?1003 ?1004 ?1006滚轮wheel加速,最多 6 行/次accelerated, ≤ 6 lines选择复制select & copyOSC 52搜索searchalt-screen-search.ts布局layoutHStack · VStack · ScrollView退出exit\x1b[?1049l12345regularfullscreen输出output

备用屏一帧依次经过:布局帧 → 搜索高亮 → 回到底部提示 → 覆盖层 → 选区 → 短暂提示 → 光标与行尾重置,最后只写变化的行。An alternate-screen frame goes through: layout frame → search highlights → jump-to-end pill → overlays → selection → flashes → cursor and line resets, then only changed rows are written.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.004
04 · 输入与按键04 · Input and keyspi-tui

输入与按键Input and keys

Input: from stdin bytes to focused.handleInput()TuiBase.handleTerminalInput(data)依次尝试,可被消费in order; may be consumedkeys.ts把序列认成按键名turn sequences into key namesKeybindingsManager47 个 tui.* 按键 id47 tui.* key idsprocess.stdin原始字节raw bytesProcessTerminalsetRawMode · ?2004h 括号粘贴setRawMode · ?2004h bracketed pasteKitty 键盘协议 flags = 7Kitty keyboard protocol, flags = 7StdinBuffer拆成完整转义序列 · 粘贴整段保留split into whole sequences · paste kept wholeESC 10ms(SSH 100ms)· 序列 50msESC 10 ms (SSH 100 ms) · sequence 50 ms终端颜色回复terminal colour replyOSC 10 / 11 / 4配色方案通知colour-scheme report深/浅色切换dark / light switchaddInputListener()consume 或改写 dataconsume or rewrite data单元格尺寸回复cell-size reply图片用for imagesshift+ctrl+d调试回调 onDebugonDebug hook覆盖层焦点修复overlay focus repairvisible() 变化后after visible() changes过滤按键释放drop key releases除非 wantsKeyReleaseunless wantsKeyReleasefocused.handleInput(data)然后立即渲染then render immediately序列举例SAMPLE SEQUENCES\x1b[A up\x1b[13;2u shift+enter\x1b[99;5:3u ctrl+c release\x1b[200~ … \x1b[201~ paste\x1b]11;rgb:… bg colour replyKitty 查询 \x1b[>7u\x1b[?u 无回应时,If the Kitty query \x1b[>7u\x1b[?u gets no reply在 DA 回复后改用 modifyOtherKeys \x1b[>4;2mbefore DA, fall back to modifyOtherKeys \x1b[>4;2mmatchesKey(data, "ctrl+c")Kitty CSI-u · 旧式 · modifyOtherKeysKitty CSI-u · legacy · modifyOtherKeysparseKey(data)→ "shift+enter" 等→ "shift+enter" etc.isKeyRelease(data)Kitty 事件类型 3Kitty event type 3decodeKittyPrintable(data)CSI-u → 可打印字符CSI-u → printable texttui.input.*4tui.select.*6tui.editor.*23tui.altScreen.*14input.submitenterinput.newLineshift+enter / ctrl+jinput.copyctrl+ceditor.undoctrl+-editor.yankctrl+y用户覆盖:~/.pi/agent/keybindings.jsonOverrides: ~/.pi/agent/keybindings.json12345678终端层terminal layerTUI 分发TUI dispatch组件 · 按键component · keys

单独的 ESC 要等 10ms 才能确定是 Escape 键而不是序列开头;SSH 下默认 100ms,可用 PI_TUI_ESC_TIMEOUT 调整。A lone ESC needs 10 ms to be sure it is the Escape key rather than the start of a sequence; 100 ms over SSH, adjustable with PI_TUI_ESC_TIMEOUT.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.005
05 · 编辑器与自动补全05 · The editor and autocompletepi-tui

编辑器与自动补全The editor and autocomplete

The Editor and autocomplete细节DetailsEditor.handleInput(data)自上而下尝试,命中即返回tried top to bottom; first match returns自动补全AutocompleteCombinedAutocompleteProvider«class» implements Component, FocusableEditor−state: { lines[], cursorLine, cursorCol }−undoStack: UndoStack<EditorState>−killRing: KillRing−history: string[] (≤ 100)−pastes: Map<id, text>+onSubmit?(text) · onChange?(text)+getText() · setText() · insertTextAtCursor()+setAutocompleteProvider(p)+addToHistory(text)跳转模式jump modejumpForward括号粘贴bracketed paste\x1b[200~交给上层to parentctrl+c撤销undoctrl+-补全菜单completion menuup · down · tab · enterTab 触发Tab triggerforce: true删除delete字符 · 单词 · 到行首尾char · word · linekill ringctrl+y · alt+y历史historyup · down光标移动cursor行首尾 · 按词line · word换行new lineshift+enter提交submitenter → onSubmit其余:插入字符else: insert textdecodeKittyPrintable/行首的斜杠命令slash command at line startfuzzyFilter(commands)@文件附件file attachmentfd 搜索 · 防抖 20msfd search · 20 ms debounceTab路径补全path completionforce: true · 读目录force: true · readdirSelectList弹出在编辑器上方 · ↑↓ 选择pops above the editor · ↑↓ to pickapplyCompletion(…)→ { lines, cursorLine, cursorCol }大段粘贴折叠成标记Large pastes collapse to a marker[paste #1 +42 lines]阈值threshold> 10 行或 > 1000 字符> 10 lines or > 1000 chars提交时on submit标记换回原文markers expand back行尾 \ + Entertrailing \ + Enter插入换行而不是提交inserts a newline instead撤销undoUndoStack 保存状态快照UndoStack keeps state snapshots历史history最多 100 条up to 100 entrieskill ring按行/按词删除的文本,连续删除合并line / word kills; consecutive ones merge确认confirm提供者接口:triggerCharacters? · getSuggestions(lines, line, col, { signal, force }) · applyCompletion(…)Provider interface: triggerCharacters? · getSuggestions(lines, line, col, { signal, force }) · applyCompletion(…)12345678910111213编辑器状态 / 提交editor state / submit按键分支key branches补全completion

超过 10 行或 1000 字符的粘贴显示为 [paste #1 +42 lines] 这样的标记,提交时再换回原文——编辑器本身保持轻量。Pastes over 10 lines or 1000 characters show as markers like [paste #1 +42 lines] and expand back on submit — the editor itself stays light.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.006
06 · 覆盖层06 · Overlayspi-tui

覆盖层Overlays

Overlays: showOverlay(component, options)9 个锚点9 anchors终端可用区域(去掉 margin)terminal area minus margin每一帧Every framecompositeOverlays()焦点Focus覆盖层是一个栈overlays form a stack«interface»OverlayOptions+width?: number | "50%"+minWidth?: number+maxHeight?: number | "%"+anchor?: 9 values+offsetX? · offsetY?+row? · col?: number | "%"+margin?: number | { top … }+visible?(w, h): boolean+nonCapturing?: boolean«returned»OverlayHandle+hide()+setHidden()+isHidden()+focus()+unfocus()+isFocused()+getBounds()筛选可见的filter visiblevisible(w, h) · 未隐藏visible(w, h) · not hidden解析宽度resolve width% / minWidth / margincomponent.render(width)截到 maxHeightcut to maxHeight解析 row / colresolve row / col用实际高度算锚点anchor with real heightcompositeLineAt(…)按列拼进底层行splice into base linesshowOverlay()记住 preFocus,然后抢焦点remember preFocus, take focusnonCapturing: true显示但不抢焦点shown, focus untouchedhide()焦点交给下一个可见覆盖层或 preFocusfocus to next visible overlay or preFocustop-lefttop-centertop-rightleft-centercenterright-centerbottom-leftbottom-centerbottom-right默认:center · 宽 min(80, 可用宽度)default: center · width min(80, available)每次输入前都会复查:focused 覆盖层若已不可见(resize 或 visible() 变 false),焦点改到最上层可见覆盖层或 preFocus。Before every input: if the focused overlay is no longer visible (resize, or visible() turned false), focus moves to the top visible overlay or preFocus.覆盖层按 focusOrder 排序叠放;focus() 把一个覆盖层提到最上层。Overlays stack by focusOrder; focus() brings one to the top.12345API定位placement合成compositing焦点focus

默认居中,宽度为 min(80, 可用宽度);width / maxHeight / row / col 都接受数字或 "50%" 这样的百分比。Default: centred, width min(80, available); width / maxHeight / row / col accept a number or a percentage such as "50%".

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.007
07 · 终端能力07 · Terminal capabilitiespi-tui

终端能力Terminal capabilities

Terminal capabilities: negotiation, output sequences, imagesProcessTerminal.start()Terminal 接口 13 个成员Terminal interface: 13 members输出与查询Output & queriesdetectCapabilities()按环境变量识别;PI_IMAGE_PROTOCOL · PI_TRUE_COLOR · PI_HYPERLINKS 可覆盖from env vars; PI_IMAGE_PROTOCOL · PI_TRUE_COLOR · PI_HYPERLINKS overridesetRawMode(true)逐字节读取,不回显byte-wise input, no echo\x1b[?2004h开启括号粘贴bracketed paste on\x1b[>7u \x1b[?u请求 Kitty 键盘协议并查询push Kitty flags 7 and query\x1b[cDA:用作"查询结束"哨兵DA as the end-of-queries sentinel\x1b[>4;2m无 Kitty 回复时的 modifyOtherKeysmodifyOtherKeys when Kitty is silentOSC 0OSC 9;4OSC 7501OSC 10/11OSC 8?2026kittyiterm2Windows:额外打开 VT 输入;stop() 恢复原 raw 状态并关掉以上模式。Windows: also enables VT input; stop() restores raw state and undoes the modes.\x1b]0;title\x07窗口标题window title\x1b]9;4;3 / 0任务栏进度(不确定 / 清除)taskbar progress (busy / clear)idle · working · blocked …程序状态(终端声明支持时)program status (if supported)前景 / 背景色查询fg / bg colour query自动判断深浅主题picks dark or light超链接hyperlinks只在确认支持时启用only where known to work同步输出synchronized output每帧包起来,防闪烁wraps every frame终端TERMINAL图片协议IMAGES真彩色TRUECOLOR超链接HYPERLINKSkitty · ghostty · wezterm · warp✓✓iTerm2✓✓Windows Terminal · alacritty · vscode · zed—✓✓JetBrains · Windows console—✓—tmux—COLORTERM探测probedscreen · 未知终端screen · unknown—COLORTERM—12345启动协商start-up negotiation输出序列output sequences能力识别capability detection

图片:kitty、ghostty、wezterm、warp 用 Kitty 图形协议,iTerm2 用自己的协议;tmux、screen 与其他终端不显示图片。Images: kitty, ghostty, wezterm and warp use the Kitty graphics protocol, iTerm2 its own; tmux, screen and other terminals show no images.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.008
08 · 模块结构08 · Module structurepi-tui

模块结构Module structure

pi-tui modules (19,409 lines of TypeScript)终端与输入Terminal & input渲染核心Render core编辑Editing文本与图形Text & graphicscomponents/:开箱即用的组件components/: ready-made components18 个文件18 files609terminal.tsProcessTerminal · 协议协商ProcessTerminal · negotiation444stdin-buffer.ts字节流 → 完整序列bytes → whole sequences1,401keys.tsparseKey · matchesKey320keybindings.ts可配置的按键 idconfigurable key ids65native-platform.ts剪贴板 · 修饰键(可选)clipboard · modifiers (opt.)53program-status.tsOSC 7501 程序状态OSC 7501 program status1,493tui.tsComponent · TuiBase · 覆盖层Component · TuiBase · overlays655tui-main-screen.tsregular:行级差分regular: line diff1,784tui-alt-screen.tsfullscreen:备用屏fullscreen: alt screen449layout.ts布局帧 · 滚动条layout frame · scrollbars327alt-screen-search.ts全屏内搜索in-screen search82wheel-scroll.ts滚轮加速wheel acceleration2,472editor.ts多行编辑器multi-line editor861autocomplete.ts/ 命令 · @ 文件 · 路径/ commands · @ files · paths28undo-stack.ts快照式撤销snapshot undo46kill-ring.tsEmacs 式 kill/yankEmacs kill / yank138fuzzy.ts模糊匹配打分fuzzy scoring117word-navigation.ts按词移动word motion1,397utils.tsvisibleWidth · 截断 · 换行visibleWidth · truncate · wrap1,025markdown.tsMarkdown → ANSI1,506latex.tsLaTeX → Unicode763terminal-image.tsKitty / iTerm2 图片Kitty / iTerm2 images367colors.ts颜色转换colour conversion91terminal-colors.ts前景/背景色探测fg / bg detectionalt-screen-flash 51box 172cancellable-loader 40editor 2,472h-stack 44image 167input 494loader 101markdown 1,025mouse-region 33scroll-view 224select-list 273settings-list 328spacer 28stack 154text 113truncated-text 65v-stack 33标签 = 行数tag = lines of code终端terminal渲染render编辑editing文本 · 图形text · graphics

最大的文件是 components/editor.ts:多行编辑、撤销、kill ring、历史、粘贴折叠与补全菜单都在这里。The largest file is components/editor.ts: multi-line editing, undo, the kill ring, history, paste folding and the completion menu all live there.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.009
扩展点Extension pointspi-coding-agent

tui 的扩展点Where to plug into tui

1
ComponentComponent

实现 render(width) 即可成为组件;可选 handleInput / handleMouse。Implement render(width) to be a component; handleInput / handleMouse are optional.

2
AutocompleteProviderAutocompleteProvider

替换或组合补全来源。Replace or combine completion sources.

3
KeybindingsManagerKeybindingsManager

覆盖默认键位,或增加自己的按键 id。Override default bindings or add your own key ids.

4
showOverlay(component, options)showOverlay(component, options)

对话框、选择器、提示浮层。Dialogs, pickers, pop-ups.

5
addInputListener()addInputListener()

在组件之前拦截或改写输入。Intercept or rewrite input before components see it.

图解 Pi Agent · Pi v1.1.0Pi Agent, Illustrated · Pi v1.1.010
←
1 / 10