9.1 KiB
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 前必读):
- 默认不带 cookie 直接连;只有连接被拒才
osascript取一个新的并重试一次(此时会在同一进程内立刻用掉,才享受得到 0.05s)。 - 若环境里已经继承了
ITERM2_COOKIE(别的脚本 export 的),先 unset:它多半已是一次性令牌用过的旧值,留着就是走 0.55s 的慢路径。 (历史 bug:旧入口把 cookie 导出成ITERM_COOKIE,而 iterm2 库读的是ITERM2_COOKIE—— 白付 0.8s 还没生效。) - 调用方要合并调用:单条命令 0.5s 里 0.3s 是连接。同一件事能用
batch一次连完就一次连完(X=$(cmd)可捕获输出)。 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。
配方:把一条消息投给另一个并发会话
- 用
pi-skill iterm-workspace-ctl ids找目标会话 ID,按标题里的项目或主题辨认。 - 运行
pi-skill iterm-workspace-ctl send-when-idle start <id> "<文本>":它先连续静止--quiet 120秒,再用--confirm 30秒复核后发送并回车,不会打断对方当前这轮。 - 文本会成为对方的用户消息,必须自足地说明谁发的、发现什么、结论或修法和证据位置(提交号或文件路径),并控制在一屏内。
- 用
send-when-idle status|logs <id>|stop <id>查守候台账或停止守候;stop只杀守候进程,不动目标会话。 - 不要用这条通道发送密码或令牌。
标签/窗口标题为什么显示正在运行的命令,怎么关
现象:~ (-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)。