iterm-workspace-ctl/skills/iterm-workspace-ctl/REFERENCE.md
2026-10-02 09:26:23 +08:00

9.1 KiB
Raw Blame History

iTerm 控制入口维护

当前实现使用 cookie + websocket Python API,不再使用旧 launch API script named 通道。STATUS.md 是旧通道的历史记录,不代表当前实现。

  • 常用入口:bash scripts/iterm-ctl.sh <命令>;无参数或 help 显示帮助,help <命令> 显示详情。
  • 首次初始化:bash scripts/install.sh run。它会修改 iTerm API 偏好并安装依赖,仅在需要且用户授权时运行;迁移恢复不需要重新授权。
  • 依赖路径由 bash scripts/python-library-path.sh path 解析,默认 ~/Library/Caches/iterm-workspace-ctl/<Python ABI>-<架构>,不放共享仓库。
  • 可用 ITERM_CTL_PYTHON 指定解释器、ITERM_CTL_PY_LIB 指定私有依赖目录、XDG_CACHE_HOME 指定缓存根目录。所有入口复用同一个路径原语。
  • 缺库时命令入口会安装到该缓存;网络失败应报告,不回退到旧 GUI 通道或把二进制提交到配置仓库。
  • 常用只读查询:where(当前位置)、tab-list(带 current±N 相对位置)、tab-show / win-show(范围详情)、win-list、capture-tab(屏幕/回滚历史)。它们直接输出可读结果,不要让调用方写脚本解析;tree [--json] 仅供机器处理。capture 会读取屏幕正文,不用于无关的健康检查。不要为了验证恢复而分屏、发命令或关闭用户窗格。
  • 改本技能后跑 python3 tests/test_entrypoints.py(不连真实 iTerm,用假模块);真实只读冒烟:tree、where、frame。
  • close/close-others/win-close 会终止进程,必须确定用户目标,不用于测试。
  • 同步只提交技能源码。迁移前的本机库可复用到相同解释器/架构的缓存,不能把 macOS 库复制给 Windows 用。

性能事实(改入口前先读,别把慢路径加回来)

实测(同一台机,var 这种单命令经 pi-skill 调用):

环节 耗时 说明
pi-skill 包装本身 0.12s node,可忽略
解析 python 库路径 0.10s 已缓存到 ~/Library/Caches/iterm-workspace-ctl/.libpath
osascript 现取 cookie 0.8s 最大的单项浪费
websocket 连接(不带 cookie) 0.27s 地板
websocket 连接(新 cookie,同一次进程内) 0.05s 只有紧跟的那次连接快
websocket 连接(复用旧 cookie) 0.55s 最慢:iTerm2 走回退路径

由此定下的入口规矩(改 iterm-ctl.sh 前必读):

  1. 默认不带 cookie 直接连;只有连接被拒才 osascript 取一个新的并重试一次(此时会在同一进程内立刻用掉,才享受得到 0.05s)。
  2. 若环境里已经继承了 ITERM2_COOKIE(别的脚本 export 的),先 unset:它多半已是一次性令牌用过的旧值,留着就是走 0.55s 的慢路径。 (历史 bug:旧入口把 cookie 导出成 ITERM_COOKIE,而 iterm2 库读的是 ITERM2_COOKIE —— 白付 0.8s 还没生效。)
  3. 调用方要合并调用:单条命令 0.5s 里 0.3s 是连接。同一件事能用 batch 一次连完就一次连完(X=$(cmd) 可捕获输出)。
  4. batch 的捕获只取最后一行:tree --json 是多行 JSON,不能这样捕获(实测会拿到 T={);多行数据直接输出再用管道解析。

后台 tab 的窗格尺寸不刷新(应用层坑,写自动化必踩)

var <id> columns / tree --json 里某栏的尺寸,对非活动 tab 是旧值:关栏/切栏后读到的一直是老宽度。 实测同一个窗格先后读到 19 / 58 / 177,而 focus-id 把它所在 tab 选中后立刻变成正确的 177。 所以“读几何 → 据此排版”的流程必须先 focus-id 自己,否则排版全错(longtext-panes 就踩过这个坑)。

等目标空闲再发送(wait-idle / send-when-idle)

用途:目标 tab/窗格正在跑任何东西(批量命令、pi/codex 之类的 TUI),要「等它手头的活干完,再替我发一句话/发一条命令」。 目标是通用的:任何 pane/session、任何前台程序,判定只看两条与程序无关的事实。

pi-skill iterm-workspace-ctl idle-state <pane|id>                 # 一次性看忙闲(job/prompt/screen_hash)
pi-skill iterm-workspace-ctl wait-idle <pane|id> [选项] [--send "单行文本"]   # 前台阻塞,可在脚本里组合
pi-skill iterm-workspace-ctl send-when-idle start <pane|id> "单行文本" [选项]  # 后台守候 + 台账
pi-skill iterm-workspace-ctl send-when-idle status|logs <id>|stop <id|--all>

判定语义(--when):

值 空闲条件 适用
prompt 前台是 shell(iTerm jobName=zsh/bash/fish/...)= 命令跑完、回到提示符 批量命令;静默长命令也不会误判
quiet 前台是别的程序,可见屏连续 --quiet 秒(默认 120)没变 = 停在它自己的输入框 TUI(pi/codex/vim)
auto(默认) 两者择先 通用

判定成立后还会复核 --confirm 秒(默认 30;0=立即)再发;期间屏幕又动就重新等。 --poll(默认 10s)是采样间隔,--max(默认 21600s,0=不限)是守候上限,超时退出码 1。

事实与边界:

  • 文本必须单行(含换行会在目标里提前提交);长文本用 --file(末尾换行去掉,内部换行直接拒绝)。
  • 目标可以是 pane 方位(left/right/tab N/…)或 session id;pane 方位只在 start 那一刻解析,之后守候按锚定到的 session id,不受焦点变化影响。
  • 静默不输出的长命令会被 quiet 误判成空闲;要安全用 --when prompt。发出去的文本等价于在那个窗格键入 + 回车。
  • 台账:~/.pi/agent/local/skills/iterm-workspace-ctl/jobs/。每个任务 .meta(参数/pid)、.text、.state(每轮采样,status 显示的 as_of 就是最近一次采样时间,也是它有 liveness 的证据)、.log、.result(退出码;stopped 是 stop 写的)。台账只留最近 30 个任务:更旧的、已结束的会在下次 start 时自动清掉(守候中的不删)。
  • 同一目标同时只允许一个守候任务;已经守候中的目标再 start 会直接报错(避免两条消息都发进去)。
  • stop 只杀守候进程树,不碰目标 session。
  • 单测:python3 tests/test_entrypoints.py —— 判定函数/参数解析用纯函数测,脚本 start/status/stop 用假 ctl(ITERM_CTL_CTL 指向假入口 + ITERM_CTL_JOBS_DIR 指向临时目录)测,不连真实 iTerm。

配方:把一条消息投给另一个并发会话

  1. 用 pi-skill iterm-workspace-ctl ids 找目标会话 ID,按标题里的项目或主题辨认。
  2. 运行 pi-skill iterm-workspace-ctl send-when-idle start <id> "<文本>":它先连续静止 --quiet 120 秒,再用 --confirm 30 秒复核后发送并回车,不会打断对方当前这轮。
  3. 文本会成为对方的用户消息,必须自足地说明谁发的、发现什么、结论或修法和证据位置(提交号或文件路径),并控制在一屏内。
  4. 用 send-when-idle status|logs <id>|stop <id> 查守候台账或停止守候;stop 只杀守候进程,不动目标会话。
  5. 不要用这条通道发送密码或令牌。

标签/窗口标题为什么显示正在运行的命令,怎么关

现象:~ (-zsh)、sleep (sleep)、cd (sleep) —— 标题跟着前台命令变,命令结束后恢复。两层叠加,只关一层还会显示命令:

层 来源 关闭方法
shell oh-my-zsh 的 omz_termsupport_preexec(lib/termsupport.zsh)每条命令前用 OSC 1/2 把标题改成命令,precmd 改回目录 ~/.zshrc 末尾覆盖 omz_termsupport_preexec() { title "$ZSH_THEME_TERM_TAB_TITLE_IDLE" "$ZSH_THEME_TERM_TITLE_IDLE" }(保留目录标题;想彻底固定用 DISABLE_AUTO_TITLE="true"。只对新开的 shell 生效)
iTerm2 profile 的 Title 元素带 Job 位,渲染成 名字 (job) Settings → Profiles → 该配置 → General → Title 里取消 Job;等价 API 是写 Title Components 掩码(见下)

iTerm2 3.6.9 掩码实测:bit1=2 是 Job;bit0=1 与 bit11=2048 都渲染会话名(iterm2 模块枚举只声明到 bit10=SIZE,.title_components 读到未知位会 ValueError,读值要用 _simple_get)。写共享 profile 会持久化进 plist,但已在跑的 session 掩码是建会话时的快照,要让当前屏幕立刻改(例如本会话标题的 (pi) 后缀)得逐个写会话级 profile。

# 解释器:iTerm2 自带 "$HOME/Library/Application Support/iTerm2/iterm2env-*/versions/*/bin/python3"(含 iterm2 模块)
import asyncio, iterm2
async def main():
    conn = await iterm2.Connection.async_create()
    for p in await iterm2.Profile.async_get(conn):
        m = p._simple_get('Title Components') or 0
        if p.name == 'Dark' and m & 2:                      # 2 = Job
            await p._async_simple_set('Title Components', m & ~2)
asyncio.run(main())

验证:新开 tab 跑 sleep 7,标题保持目录不变;tree 里 name= 不再出现 (job)。