SLOPSHOPPER

mods-test-lab

Local Mods test panel, bounded event summaries, and explicit one-shot prompt context. Open with /mods-test.

newpaneguardcommandprompttool
★ 55v0.1.0no licenseupdated 2026-10-24Esonhugh/claude-code/examples/mods/mods-test-lab
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mods-test-lab
│ ┃ Mods test lab ✕ › fix the failing auth test and add an audit log call │ ┃ Mods test lab | activation=1 Activation │ ┃ counters (since load/reset): ⏺ Read(src/auth.ts) │ ┃ {"command":1,"tool":9,"prompt":1,"turn":1} Pe ⎿ Read 6 lines │ ┃ rsistent counters (since reset; activations ⏺ Update(src/auth.ts) │ ┃ retained): ⎿ Added 2 lines, removed 1 line │ ┃ {"activations":1,"command":1,"tool":9,"promp ⏺ Bash(bun test) │ ┃ t":1,"turn":1} Controls: ⎿ 3 pass, 1 fail │ ┃ {"button":0,"input":0,"submit":0,"select":0} │ ┃ Input length=0/256 | selection=ascii | ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ context=idle Last turn: │ ┃ {"reason":"answer","aborted":false} Events ✻ Worked for 42s · done 4:20 PM │ ┃ (13/20): activation, prompt, tool, tool, │ ┃ tool, tool, tool, tool, tool, tool, tool, › /mods-test │ ┃ turn, open │ ┃ Tab/Shift+Tab: focus | Enter: │ ┃ activate/submit | arrows/wheel: scroll | │ ┃ Esc: close │ ┃ [ Count: 0 ] │ ┃ Local input: Not logged or persisted (max 25 │ ┃ Diff labels: ASCII ▾ │ ┃ [ Close panel ] │ ┃ Fixed ASCII / CJK diff │ ┃ --- before.txt +++ after.txt @@ -1,2 +1,2 @@ │ ┃ - hello world - 旧行:你好世界 │ ┃ + hello mods + 新行:宽度测试 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Mods test lab
Mods test lab | activation=1 Activation counters (since load/reset): {"command":1,"tool":9,"prompt":1,"turn":1} Persistent counters (since reset; activations retained): {"activations":1,"command":1,"tool":9,"prompt":1,"turn":1} Co ntrols: {"button":0,"input":0,"submit":0,"select":0} Input length=0/256 | selection=ascii | context=idle Last turn: {"reason":"answer","aborted":false} Events (13/20): activation, prompt, tool, tool, tool, tool, tool, tool, tool, tool, tool, turn, open Tab/Shift+Tab: focus | Enter: activate/submit | arrows/wheel: scroll | Esc: close [ Count: 0 ] Local input: Not logged or persisted (max 256) Diff labels: ASCII ▾ [ Close panel ] Fixed ASCII / CJK diff --- before.txt +++ after.txt @@ -1,2 +1,2 @@ - hello world - 旧行:你好世界 + hello mods + 新行:宽度测试 Row 01 | fixed scroll fixture | 中文宽度测试 Row 02 | fixed scroll fixture | 中文宽度测试 Row 03 | fixed scroll fixture | 中文宽度测试 Row 04 | fixed scroll fixture | 中文宽度测试 Row 05 | fixed scroll fixture | 中文宽度测试 Row 06 | fixed scroll fixture | 中文宽度测试 Row 07 | fixed scroll fixture | 中文宽度测试 Row 08 | fixed scroll fixture | 中文宽度测试 Row 09 | fixed scroll fixture | 中文宽度测试 Row 10 | fixed scroll fixture | 中文宽度测试 Row 11 | fixed scroll fixture | 中文宽度测试 Row 12 | fixed scroll fixture | 中文宽度测试 Row 13 | fixed scroll fixture | 中文宽度测试 Row 14 | fixed scroll fixture | 中文宽度测试 Row 15 | fixed scroll fixture | 中文宽度测试 Row 16 | fixed scroll fixture | 中文宽度测试 Row 17 | fixed scroll fixture | 中文宽度测试 Row 18 | fixed scroll fixture | 中文宽度测试 Row 19 | fixed scroll fixture | 中文宽度测试 Row 20 | fixed scroll fixture | 中文宽度测试 Row 21 | fixed scroll fixture | 中文宽度测试 Row 22 | fixed scroll fixture | 中文宽度测试 Row 23 | fixed scroll fixture | 中文宽度测试 Row 24 | fixed scroll fixture | 中文宽度测试 Row 25 | fixed scroll fixture | 中文宽度测试 Row 26 | fixed scroll fixture | 中文宽度测试 Row 27 | fixed scroll fixture | 中文宽度测试 Row 28 | fixed scroll fixture | 中文宽度测试 Row 29 | fixed scroll fixture | 中文宽度测试 Row 30 | fixed scroll fixture | 中文宽度测试 Row 31 | fixed scroll fixture | 中文宽度测试 Row 32 | fixed scroll fixture | 中文宽度测试 Row 33 | fixed scroll fixture | 中文宽度测试 Row 34 | fixed scroll fixture | 中文宽度测试 Row 35 | fixed scroll fixture | 中文宽度测试 Row 36 | fixed scroll fixture | 中文宽度测试 Row 37 | fixed scroll fixture | 中文宽度测试 Row 38 | fixed scroll fixture | 中文宽度测试 Row 39 | fixed scroll fixture | 中文宽度测试 Row 40 | fixed scroll fixture | 中文宽度测试 Row 41 | fixed scroll fixture | 中文宽度测试 Row 42 | fixed scroll fixture | 中文宽度测试 Row 43 | fixed scroll fixture | 中文宽度测试 Row 44 | fixed scroll fixture | 中文宽度测试 Row 45 | fixed scroll fixture | 中文宽度测试 Row 46 | fixed scroll fixture | 中文宽度测试 Row 47 | fixed scroll fixture | 中文宽度测试 Row 48 | fixed scroll fixture | 中文宽度测试 Row 49 | fixed scroll fixture | 中文宽度测试 Row 50 | fixed scroll fixture | 中文宽度测试 Row 51 | fixed scroll fixture | 中文宽度测试 Row 52 | fixed scroll fixture | 中文宽度测试 Row 53 | fixed scroll fixture | 中文宽度测试 Row 54 | fixed scroll fixture | 中文宽度测试 Row 55 | fixed scroll fixture | 中文宽度测试 Row 56 | fixed scroll fixture | 中文宽度测试 Row 57 | fixed scroll fixture | 中文宽度测试 Row 58 | fixed scroll fixture | 中文宽度测试 Row 59 | fixed scroll fixture | 中文宽度测试 Row 60 | fixed scroll fixture | 中文宽度测试
README

Unofficial Claude Code

User Animated

基于 Claude Code 分发产物恢复的非官方 TypeScript/TSX 源码工作区,并持续维护本地 CLI、Agent、Workflow、OpenAI/Codex 兼容和调试能力。

本项目不是 Anthropic 官方产品、官方源码分发或官方 Claude Code release,也未获得 Anthropic 背书。公开包只分发 launcher 与对应平台二进制,不包含本仓库源码。

作者与维护者

  • 项目维护者:Esonhugh
  • 原始产品与上游实现:Anthropic Claude Code
  • 公开包:@esonhugh/claude-code

本仓库包含从公开 bundle/source map 恢复的上游代码和本地维护改动。上游归属与本地维护者身份应分别理解;完整本地变更以 CHANGELOG.md 为准。

项目概况

本项目主要用于:

  1. 保存从 Claude Code 分发产物恢复的可读 TypeScript/TSX 源码树。
  2. 提供可构建、可调试、可进行受控二次开发的本地 Claude Code CLI。
  3. 维护 Agent、Workflow、OpenAI/Codex、交互终端和会话命令等扩展。
  4. 保留恢复工程中的类型声明、stub 与 build shim,便于后续逐步替换或验证。
  5. 通过 binary-only 流程发布非官方 launcher,不公开分发本仓库源码。

开发环境与产物

  • 包管理器:Bun
  • Node.js:>=18
  • JavaScript 构建产物:dist/cli.js
  • 本地 binary:built-claude

当前发布版本为 2.1.294;发布历史、具体变更和验收边界统一记录在 CHANGELOG.md。

安装与运行

方式一:安装公开 launcher

公开包会根据 process.platform 和 process.arch 加载对应的平台二进制 optional dependency:

bun add --global @esonhugh/claude-code
claude --version

公开包仅包含 launcher 和平台二进制,不包含恢复源码。当前 release workflow 的普通平台包覆盖 Linux x64、Linux arm64、macOS arm64 和 Windows x64;不应假设其他 OS/architecture 组合(例如 macOS x64)已有产物。Linux 还会构建 baseline x64 和 musl 验证产物,但 baseline 不作为普通 npm 平台包。若当前平台没有对应 binary package,launcher 会返回缺少平台包的错误。

方式二:从源码构建

git clone <repository-url>
cd claude-code
bun install
make build
./built-claude --version

make build 构建平台 binary,并生成根目录下的 built-claude。

只构建 JavaScript CLI:

bun run build
bun ./dist/cli.js --version
bun ./dist/cli.js --help

主要功能

每项功能的作用、配置写法与可选值、不同配置下的终端效果见对应文档;完整索引见 docs/features/,变更历史见 CHANGELOG.md。设置写入用户级或项目级 settings.json,使用时与现有 JSON 合并。

领域功能说明
模型与 ProviderOpenAI / Codex provider把 Claude Code 的模型换成 OpenAI(GPT / Codex):用 ChatGPT 账号登录或 API key,工具、界面、命令照常使用
模型自动发现与 Gateway走自己的 API 网关或 OpenAI 兼容服务时,自动把网关提供的模型列进 /model,不用手打模型名
请求韧性:重试、回退模型与默认模型API 过载或断线时自动重试;主模型一直过载就换备用模型;可以让无人值守任务一直等到额度恢复
Effort 档位控制 Claude 每次「想多深」:低档快而省,高档慢而仔细;可全局设、按会话切,也可给子代理单独指定
Codex Apps用 ChatGPT 账号登录 OpenAI provider 时,直接使用 ChatGPT 里连好的 App(GitHub、Gmail 等):输入 @codex-app: 点名即可
Agent 与编排Agent 与后台任务Claude 把一部分工作交给「子代理」在后台做,你继续聊;可以查看、停止、续跑,也可以自己定义专用子代理
Agent Teams让 Claude 组一个小团队:一个负责人带几个队友并行干活,互相发消息、共享任务清单
Dynamic Workflow让 Claude 写一段编排脚本,一次调度多个子 Agent 分阶段、并行地干活;/workflows 看进度、保存成可复用的流程
背景会话、daemon 与 agents 视图把任务交给后台跑、终端立刻空出来;claude agents 一屏看谁在等你、谁在干活、谁做完了
独立会话通信同一台机器上开着的几个 Claude 会话可以互相发现、互发消息:一个写代码,一个做 review,完成了喊一声
权限与安全权限规则、权限模式与 Safe modeClaude 什么时候要先问你:用规则提前放行或禁止,用模式整体调松或调紧;受保护路径在任何模式下都会问
自动模式(Auto mode)不用一条条点确认:由分类器判断每个操作是否安全,安全的直接做,危险的拦下;可以用一句话加自己的规则
Hooks 事件、匹配与输出校验在 Claude 做某件事之前或之后自动跑你的脚本:拦住危险命令、改完自动格式化、开局注入上下文
Bash 沙箱让 Claude 跑的命令只能写项目目录、只能连你允许的域名、读不到你指定的密钥;开了以后命令可以不再逐条确认
工具Terminal 工具给 Claude 一个能持续交互的终端:开 REPL、操作 TUI、按 Ctrl+C,跑完自动通知
Monitor 工具让 Claude 在后台盯着日志或命令输出,有新内容就自动叫醒它,不用反复 sleep 轮询
定时任务:Cron、/loop 与 ScheduleWakeup让 Claude 按时间自动做事:「每 5 分钟看一下构建」「工作日早上 9 点汇总 PR」;/loop 一句话设定
终端程序状态(OSC 7501)支持的终端能在标签页上显示 Claude 正在忙、在等你批准、还是已经做完,不用切回来看
上下文与记忆记忆:auto memory 与 SQLite FTS5 检索Claude 把跨会话有用的事(项目约定、你的偏好)记到文件里,下次自动带上;可改成 SQLite 全文检索,按需查找
自动压缩窗口对话太长时把早期内容压成摘要;/autocompact 决定多长时压缩(按模型分别保存),/compact 手动压缩
Prompt context 与 Tool search让每次请求更省 token:提示词分层利于缓存,MCP 工具用到时才加载,大量同类工具只发一行汇总
用量、成本与状态/usage 看本次会话花了多少钱、用了多少 token;/stats 看长期使用习惯;恢复会话后成本继续累计
界面与交互会话命令本 fork 新增或补齐的斜杠命令:/recap 回顾、/cd 换目录、/focus 精简显示、/autocompact 调压缩时机、/usage 用量等
自主 Goal给 Claude 一个「做到才算完」的条件(如「所有测试通过」),每轮结束时自动检查,没达成就继续干
快捷键与 Vim 模式自己改快捷键、回合中途排队或立即插话、在输入框里用 Vim 的选择和括号跳转
自定义 UI / Branding 与 Status line改界面上显示的产品名、换掉 Logo 的 ASCII 图,用自己的脚本定制底部状态栏
/diff 视图全屏右侧 diff 面板:会话 / 未提交 / 分支三种基准、按轮次回看、把单个文件 diff 附到下一条提示
调试日志遇到问题时打开详细日志,看清楚 Claude Code 在后台做了什么、请求发到了哪
遥测与反馈默认不向 Anthropic 发送使用数据,/feedback 也随之隐藏;需要时一个环境变量打开
远程执行Direct Connect在一台机器上 claude server,在另一处用 claude cc://... 连过去:工具在 server 上执行,界面和权限确认在你这边
SSH Remoteclaude ssh 主机 让 Claude 在远程 Linux 机器上读写文件、跑命令,界面和权限确认留在本机;API 凭据不离开本机
插件与扩展插件与 Marketplace从插件市场一键安装命令、skill、agent、hook 和 MCP;团队可以自建 marketplace,装完 /reload-plugins 立即生效
MCP 配置与认证接入外部工具服务器(数据库、浏览器、内部 API 等);配置错一条只跳过那一条,/mcp 查看状态、重连、登录
Skills把一套做事步骤写进 SKILL.md,用 /名字 调用,或让 Claude 在合适时自动使用;/skill-doctor 找出从没用过却占提示的 skill
Channels让 Telegram、Discord 或你自己写的 MCP server 把消息推进正在运行的会话,还能在手机上远程批准工具调用
Mods / Function Hooks用一个 JS/TS 文件改造 Claude 本身:加命令、拦截或改写工具调用、在界面上加状态条和侧边面板(基线官方 2.1.303)
Mods 远程客户端(stream-json)给桌面 / IDE 等外部界面用的协议:通过 stream-json 让 Mod 把界面画到你自己的前端上,并把点击、输入传回来
Agent SDK 与 stream-json 控制协议在自己的程序里调用 Claude:官方 Agent SDK(TS / Python)可以直接驱动本 fork 的 CLI;补齐了 SDK 0.3 会用到的控制请求
VS Code 插件让官方 VS Code 插件改用本 fork 运行:面板里的 /status、/usage、/memory 等对话框都能正常打开
开发与发布构建、发布与终端验收工具给贡献者:怎么从源码构建出可运行的 binary、怎么和官方版本对照、发布流程做了什么

开发约定

修改 TypeScript 后至少运行:

bunx tsc --noEmit --pretty false
bun run build
bun run lint
bun run audit:missing
git diff --check

涉及 binary、CLI 入口或交互行为时,再运行:

make build
./built-claude --version
./built-claude --help

本项目仍包含恢复阶段的类型边界。修复类型时应优先使用精确 interface、discriminated union、unknown、assertion function 和 type guard,避免为消除局部错误而放宽全局核心类型。

技术文档(Ref)

入门与构建

架构

SSH、Workflow、研究与历史

研究和归档文档描述的是特定时间点,不应直接视为当前行为保证;当前使用方法以 README、docs/features/ 和源码为准,历史与验收记录以 CHANGELOG 为准。

安全与适用范围

本仓库用于授权的研究、调试和二次开发。恢复或修改后的 binary 不应未经独立安全、遥测、更新和权限审查直接用于生产环境。

请勿提交 .env、~/.codex/auth.json、API key、OAuth token、cookie、证书或其他私有配置。涉及外部 provider、插件、MCP、Workflow 和自动化任务时,应先确认权限范围及其对本地或共享环境的影响。

Source 1 files
hooks/register.ts 213 lines
1import type { On } from 'claude-code'
2
3const paneId = 'mods-test-lab'
4const contextMarker = '[mods-test-lab: one-shot context]'
5const usage = '/mods-test [open|background|status|reset|close|context|register|toolsearch NONCE|suggest TEXT]'
6const generation = 'base'
7const emptyCounts = () => ({ command: 0, tool: 0, prompt: 0, turn: 0 })
8type Event = 'activation' | 'open' | 'status' | 'reset' | 'close' | 'context' |
9  'tool' | 'prompt' | 'prompt.context' | 'turn' | 'button' | 'input' | 'submit' | 'select'
10
11let counts = emptyCounts()
12let persistent = { activations: 0, ...emptyCounts() }
13let controls = { button: 0, input: 0, submit: 0, select: 0 }
14let events: Event[] = []
15let input = ''
16let selection = 'ascii'
17let pendingContext = false
18let pendingToolSearch: string | undefined
19let lastTurn: { reason: string; aborted: boolean } | undefined
20
21function record(event: Event) {
22  events.push(event)
23  if (events.length > 20) events.shift()
24}
25
26function summary() {
27  return [
28    `Mods test lab | activation=${persistent.activations}`,
29    `Activation counters (since load/reset): ${JSON.stringify(counts)}`,
30    `Persistent counters (since reset; activations retained): ${JSON.stringify(persistent)}`,
31    `Controls: ${JSON.stringify(controls)}`,
32    `Input length=${input.length}/256 | selection=${selection} | context=${pendingContext ? 'pending' : 'idle'}`,
33    `Last turn: ${lastTurn ? JSON.stringify(lastTurn) : 'none'}`,
34    `Events (${events.length}/20): ${events.join(', ')}`,
35  ].join('\n')
36}
37
38export function register(on: On) {
39  on('session.start', async ($, e, next) => {
40    const saved = await $.store.get('counters')
41    if (saved && typeof saved === 'object' && !Array.isArray(saved)) {
42      for (const key of Object.keys(persistent) as (keyof typeof persistent)[]) {
43        const value = (saved as Record<string, unknown>)[key]
44        if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) persistent[key] = value
45      }
46    }
47    persistent.activations++
48    await $.store.set('counters', persistent)
49    await $.command.register({
50      name: 'mods-test',
51      description: 'Open the local Mods test lab (no model request)',
52      argumentHint: '[open|background|status|reset|close|context|register|toolsearch NONCE|suggest TEXT]',
53      immediate: true,
54    })
55    record('activation')
56    return next(e)
57  })
58
59  on('command.run', { command: 'mods-test' }, async ($, e) => {
60    const [action = 'open', argument] = e.args.trim() ? e.args.trim().split(/\s+/, 2) : []
61    if (action === 'toolsearch') {
62      if (!argument || !/^[A-Za-z0-9_-]{1,32}$/.test(argument)) return { text: usage }
63      pendingToolSearch = argument
64      return { text: `MODS_TOOLSEARCH_ARMED:${argument}` }
65    }
66    if (action === 'suggest') {
67      if (!argument || !/^[A-Za-z0-9_-]{1,32}$/.test(argument)) return { text: usage }
68      const result = await $.prompt.suggest({ text: `MODS_SUGGESTION:${generation}:${argument}` })
69      return { text: `suggestion=${result.isShown ? 'shown' : 'hidden'}:${generation}:${argument}` }
70    }
71    if (!['open', 'background', 'status', 'reset', 'close', 'context', 'register'].includes(action)) return { text: usage }
72    if (action === 'register') {
73      try {
74        const registered = await $.tool.register({
75          name: 'acceptance-probe',
76          description: 'Compiled native sec-default policy acceptance probe',
77        })
78        return { text: `registration=${registered.tool}` }
79      } catch (error) {
80        return { text: `registration=${error instanceof Error ? error.message : String(error)}` }
81      }
82    }
83    if (action === 'reset') {
84      counts = emptyCounts()
85      persistent = { activations: persistent.activations, ...emptyCounts() }
86      controls = { button: 0, input: 0, submit: 0, select: 0 }
87      events = []
88      input = ''
89      selection = 'ascii'
90      pendingContext = false
91      lastTurn = undefined
92    } else {
93      counts.command++
94      persistent.command++
95      record(action as Event)
96    }
97    if (action === 'context') pendingContext = true
98    await $.store.set('counters', persistent)
99    if (action === 'open') await $.ui.open({ id: paneId, title: 'Mods test lab', focus: true, closeOnEscape: true })
100    if (action === 'background') await $.ui.open({ id: paneId, title: 'Mods test lab', closeOnEscape: true })
101    if (action === 'close') await $.ui.close({ id: paneId })
102    $.ui.invalidate('ui.render')
103    if (action === 'status' || action === 'reset') return { text: summary() }
104    if (action === 'context') return { text: 'Mods test lab: fixed context armed for the next prompt only.' }
105    return {}
106  })
107
108  on('tool.describe', async ($, e, next) => {
109    const result = await next(e)
110    return /^mcp__mods-test-lab__dynamic_[A-Za-z0-9_-]{1,32}$/.test(e.tool)
111      ? { ...result, isDeferred: true }
112      : result
113  })
114
115  on('tool.call', async ($, e, next) => {
116    counts.tool++
117    persistent.tool++
118    record('tool')
119    await $.store.set('counters', persistent)
120    $.ui.invalidate('ui.render')
121    if (e.tool === 'ToolSearch' && pendingToolSearch) {
122      const nonce = pendingToolSearch
123      const expected = `select:mcp__mods-test-lab__dynamic_${nonce}`
124      if (e.query === expected) {
125        pendingToolSearch = undefined
126        await $.tool.register({
127          name: `dynamic_${nonce}`,
128          description: `Mods test lab dynamic tool ${nonce}`,
129          inputSchema: {
130            type: 'object',
131            properties: { value: { type: 'string' } },
132            required: ['value'],
133          },
134        })
135      }
136    }
137    const dynamic = /^mcp__mods-test-lab__dynamic_([A-Za-z0-9_-]{1,32})$/.exec(e.tool)
138    if (dynamic && e.value === dynamic[1]) return { result: `MODS_DYNAMIC_RESULT:${dynamic[1]}` }
139    return next(e)
140  })
141
142  on('prompt.submit', async ($, e, next) => {
143    const attach = pendingContext
144    pendingContext = false
145    counts.prompt++
146    persistent.prompt++
147    record(attach ? 'prompt.context' : 'prompt')
148    await $.store.set('counters', persistent)
149    $.ui.invalidate('ui.render')
150    return next(attach ? { ...e, context: [...(e.context ?? []), contextMarker] } : e)
151  })
152
153  on('turn.complete', async ($, e, next) => {
154    counts.turn++
155    persistent.turn++
156    lastTurn = {
157      reason: ['answer', 'aborted', 'refusal', 'error'].includes(e.reason) ? e.reason : 'unknown',
158      aborted: e.isAborted === true,
159    }
160    record('turn')
161    await $.store.set('counters', persistent)
162    $.ui.invalidate('ui.render')
163    return next(e)
164  })
165
166  on('ui.render', { component: 'Pane', requestId: paneId }, ($, e, next) => {
167    if (e.surface !== 'terminal') return next(e)
168    const { Box, Text, Button, Input, Select } = $.ui.resolve(e)
169    return Box({ flexDirection: 'column', children: [
170      Text({ children: summary() }),
171      Text({ dimColor: true, children: 'Tab/Shift+Tab: focus | Enter: activate/submit | arrows/wheel: scroll | Esc: close' }),
172      Button({ key: 'count', autoFocus: true, label: `Count: ${controls.button}`, onPress: async () => {
173        controls.button++
174        record('button')
175        $.ui.invalidate('ui.render')
176      } }),
177      Input({ key: 'input', label: 'Local input', placeholder: 'Not logged or persisted (max 256)', value: input,
178        onInput: async value => {
179          input = value.slice(0, 256)
180          controls.input++
181          record('input')
182          $.ui.invalidate('ui.render')
183        },
184        onSubmit: async value => {
185          input = value.slice(0, 256)
186          controls.submit++
187          record('submit')
188          $.ui.invalidate('ui.render')
189        },
190      }),
191      Select({ key: 'selection', label: 'Diff labels', value: selection,
192        options: [{ value: 'ascii', label: 'ASCII' }, { value: 'cjk', label: '中文 / CJK' }],
193        onSelect: async value => {
194          if (value !== 'ascii' && value !== 'cjk') return
195          selection = value
196          controls.select++
197          record('select')
198          $.ui.invalidate('ui.render')
199        },
200      }),
201      Button({ key: 'close', label: 'Close panel', onPress: async () => {
202        record('close')
203        await $.ui.close({ id: paneId })
204      } }),
205      Text({ bold: true, children: selection === 'cjk' ? '固定示例差异 / CJK diff' : 'Fixed ASCII / CJK diff' }),
206      Text({ children: '--- before.txt\n+++ after.txt\n@@ -1,2 +1,2 @@' }),
207      Text({ color: 'red', children: '- hello world\n- 旧行:你好世界' }),
208      Text({ color: 'green', children: '+ hello mods\n+ 新行:宽度测试' }),
209      ...Array.from({ length: 60 }, (_, index) => Text({ children: `Row ${String(index + 1).padStart(2, '0')} | fixed scroll fixture | 中文宽度测试` })),
210    ] })
211  })
212}
213